SendSpin ESP32
Hardware
Waveshare ESP32-S3-Zero
PCM5102A DAC
ESP32 ↔ PCM5102A Pin Connection Table
| PCM5102A Pin | ESP32 Pin | Function |
|---|---|---|
| VCC | 5V | Power input for the DAC module |
| GND | GND | Common ground |
| LCK | GPIO2 | I2S Word Select (Left/Right clock) |
| BCK | GPIO1 | I2S Bit Clock |
| DIN | GPIO3 | I2S Data Out from ESP32 |
| SCL | Not used | PCM5102A can run without SCL on ESP32 |
| FLT | GND | Filter select (optional) |
| FMT | GND | Audio data format? |
| DMP | GND | Deemphasis control |
| XMT | 3.3V | Keep DAC enabled (active high) |
Notes
- FLT/FMT/DMP must be pulled to ground. Without it, the audio was very noisy.
Software
The ESPHome project is build with version 2026.3.0-dev
Also verified it can be build on 2026.4.0, but never tested it on device. It also shows deprecation warnings during build process.
Project Overview
This configuration builds a SendSpin network audio player using:
- ESP32-S3 (DevKitC-1)
- ESP-IDF framework
- External I2S DAC
- ESPHome firmware
- Streaming via SendSpin
- Designed for use with Music Assistant
The device functions as:
- A network-synced audio endpoint
- A Home Assistant media player
- A SendSpin group-capable client
Optimized for low latency, stable WiFi streaming, efficient PSRAM usage, and 44.1kHz playback.
System Architecture
Music Assistant
↓
SendSpin
↓
ESPHome media_source
↓
speaker_source
↓
I2S Audio → External DAC
The device receives audio via SendSpin, buffers it, and outputs it via I2S to an external DAC.
ESPHome Config
substitutions:
name: sendspin-esp32-s3-zero
friendly_name: Sendspin ESP32
task_stack_in_psram: "true" # Test with this set to false!
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2025.12.0
platformio_options:
board_build.flash_mode: dio
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 4MB
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_PARTITION_TABLE_SINGLE_APP: "y"
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y"
CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y"
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y"
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y"
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
#CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y" # Try this one later!
psram:
mode: quad
speed: 80MHz
logger:
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: NONE
i2s_audio:
i2s_lrclk_pin: 2
i2s_bclk_pin: 1
light:
- platform: esp32_rmt_led_strip
id: status_led
name: "Status LED"
pin: GPIO21
rgb_order: GRB
num_leds: 1
chipset: ws2812
external_components:
- source:
type: git
url: https://github.com/esphome/esphome
ref: 4de44121a84474e89eda9f609c02a270d4b12152
components: [mixer]
- source:
type: git
url: https://github.com/esphome/esphome
ref: a2fbb0ba70bb589ee7e9acc70f76d66376b5be8a
components: [resampler]
- source:
type: git
url: https://github.com/esphome/esphome
ref: a2d98e1d5e020200db8f3caf27a74a939a661dc4
components: [audio]
- source:
type: git
url: https://github.com/esphome/esphome
ref: bff22983a390352360796f8e1363127e0cd1f898
components: [media_player]
- source:
type: git
url: https://github.com/esphome/esphome
ref: 2ea7188877f537c9b37f285b7df88ea2219e0d2f
components: [mdns, sendspin]
- source:
type: git
url: https://github.com/esphome/esphome
ref: 4f1cce8d50a967cfcac24bb56360601d5af17a56
components: [http_request, media_source, speaker_source]
sendspin:
id: sendspin_hub
task_stack_in_psram: true
kalman_process_error: 0.1
media_source:
- platform: sendspin
id: sendspin_source
text_sensor:
- platform: sendspin
type: title
name: Track Title
id: track_title
- platform: sendspin
type: artist
name: Artist
id: artist_name
- platform: sendspin
type: album
name: Album
id: album_name
speaker:
- platform: i2s_audio
id: sendspin_speaker
sample_rate: 44100
i2s_dout_pin: 3
bits_per_sample: 16bit
dac_type: external
channel: stereo
timeout: never
buffer_duration: 60ms
media_player:
- platform: sendspin
id: sendspin_group_media_player
- platform: speaker_source
id: sendspin_player
media_speaker: sendspin_speaker
sources:
- sendspin_source
volume_increment: 5%
volume_initial: 50%
name: "${friendly_name} Player"
interval:
- interval: 1s
then:
- if:
condition:
media_player.is_playing: sendspin_player
then:
- light.turn_on:
id: status_led
brightness: 50%
red: 0%
green: 100%
blue: 0%
else:
- light.turn_on:
id: status_led
brightness: 30%
red: 0%
green: 0%
blue: 100%
button:
- platform: restart
name: "Restart"
Substitutions
task_stack_in_psram:
- true: More internal RAM available, slightly slower access
- false: Faster task execution, higher internal RAM usage
Recommended: Test both if tuning for ultra-low latency.
ESP32 Configuration
Board: esp32-s3-devkitc-1
Flash Size: 4MB
Framework: ESP-IDF
Key SDK Options:
- CONFIG_PARTITION_TABLE_SINGLE_APP: Maximizes app space
- CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: Prevents internal RAM pressure
- CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: Moves TLS to PSRAM
- CONFIG_ESP_TASK_WDT_TIMEOUT_S=10: Prevents watchdog resets during streaming
Optional:
- CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: Increases CPU to 240MHz for better performance.
PSRAM Configuration
Mode: Quad
Speed: 80MHz
Required for stable DMA buffering and preventing internal RAM exhaustion.
Connectivity
WiFi power_save_mode: NONE
Disables WiFi power saving to reduce latency and jitter. Highly recommended for audio streaming.
Audio Configuration
Sample Rate: 44100 Hz
Bit Depth: 16-bit
Channels: Stereo
Buffer Duration: 60ms
60ms output buffer results in low-latency playback while maintaining stability.
Lowering below 40ms may cause crackling or underruns.
SendSpin Configuration
kalman_process_error: 0.1
Lower values provide smoother synchronization.
Higher values provide faster reaction to timing corrections.
0.1 favors responsiveness.
Performance Characteristics
Estimated device-side latency:
Pause: ~60–120ms
Seek: ~100–200ms
Spotify track skip delay depends on upstream buffering in Music Assistant.
Tuning Guide
For lowest latency:
- Enable 240MHz CPU
- Test task_stack_in_psram: false
- Keep WiFi power save disabled
- Keep buffer between 40–60ms
- Match sample rate to stream (44.1kHz)
Maintenance
OTA updates enabled.
Restart button exposed to Home Assistant.
Logger active for debugging.