Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.
A phone that already knows AirPlay should not need a second app to talk to a bench speaker. This pack flashes an ESP32 (S3 with PSRAM is the comfortable path) plus an I2S DAC as a Classic AirPlay receiver: the node advertises over Bonjour, shows up as an ESPHome media_source, and feeds a speaker_source media player Home Assistant and Music Assistant already understand. Sendspin can sit on the same player so a dedicated speaker is not AirPlay-only.
Entity names and GPIO numbers are substitutions. Do not publish a live hostname, and keep Wi-Fi credentials in secrets.yaml.
AirPlay 2 and guaranteed multi-room sync are out of scope. This is AirPlay 1 (RAOP), one receiver per board.
What problem it solves
You want tap-and-stream from an iPhone, iPad, or Mac onto a cheap ESP32 speaker already in Home Assistant — without a closed-source streamer firmware, and without giving up Music Assistant on the same box.
How it works
- I2S speaker first — ESPHome
i2s_audio+ an external DAC/amp. The speaker must accept 44.1 kHz, 16-bit. A native resampler sits in front if your DAC wants another rate. Get a localmedia_playerplaying a HA file before you add AirPlay. - AirPlay as a media_source — the ESPHome-AirPlay external component registers as
platform: airplayon ESPHome’s nativemedia_sourceAPI (same shape as Sendspin). Decoded PCM goes to the speaker; the component does not seize the I2S bus from other sources. - speaker_source player — one
media_playerlistssources: [airplay_audio](and optionallysendspin_audio). A phone starting an AirPlay session selects that source; HA pause/volume use the native player. Withremote_control: true, pause / next / previous also try DACP back to the sender when that sender exposes it. - Discovery — the AirPlay
nameis what phones show in the tap-to-stream list. Bonjour / mDNS must work on the LAN (same VLAN, no client isolation). The pack never prints a device hostname in published YAML. - Sensors — optional
active(RTSP session up, including paused),client(sender identity while connected),title/artist/albumtext sensors, and drop counters (concealed_packets,late_frames,decode_errors,output_drops) for a debug card. Metadata is those sensors, not the media-player title field (ESPHome does not yet pass source metadata through). - Music Assistant — add the Sendspin source on the same player if you want MA queues as well as AirPlay. One pipeline; the native orchestrator stops the previous source when a new one starts.
One receiver per device. Only UDP ALAC at 44.1 kHz / 16-bit stereo / 352 samples per packet is accepted. A MAX98357A is mono: use one channel or a stereo DAC if you care about a real left/right image.
Hardware notes
- Board. ESP32 or ESP32-S3, ESP-IDF, PSRAM on. The default 512-frame buffer is on the order of 700 KiB of PSRAM per session. S3 boards with octal PSRAM are the least painful. Classic ESP32 without PSRAM will not hold a stable buffer.
- Amp. MAX98357A (VIN to 5 V, GND, DIN / BCLK / LRCLK, SD held high to enable) is the cheap mono path. A PCM5102A or similar I2S DAC is the stereo / line-out path. Match
channel/num_channelsto the hardware. - Pins. Use substitutions. Avoid USB and boot-strap pins on your module. The pack’s S3 example uses GPIO15 / GPIO16 / GPIO17; change them if your board silkscreen says otherwise.
- Power. Class-D amps brown out a data-only USB port. Feed 5 V that can actually supply the speaker.
- Network. 2.4 GHz is fine. Client isolation on the AP will hide the receiver from phones. Do not put the node on a guest SSID.
Snippet
Trimmed from the pack YAML. Names and pins are substitutions. Credentials stay in secrets.yaml.
substitutions:
friendly_name: Bench speaker
airplay_name: Bench AirPlay
bclk_pin: GPIO15
lrclk_pin: GPIO16
dout_pin: GPIO17
esphome:
name: bench-speaker
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
psram:
external_components:
- source: github://inventor7777/ESPHome-AirPlay
components: [airplay]
i2s_audio:
id: dac_bus
i2s_bclk_pin: ${bclk_pin}
i2s_lrclk_pin: ${lrclk_pin}
speaker:
- platform: i2s_audio
id: dac_speaker
i2s_audio_id: dac_bus
i2s_dout_pin: ${dout_pin}
dac_type: external
sample_rate: 44100
bits_per_sample: 16bit
channel: mono
num_channels: 1
media_source:
- platform: airplay
id: airplay_audio
name: ${airplay_name}
active:
name: AirPlay active
title:
name: AirPlay title
artist:
name: AirPlay artist
remote_control: true
media_player:
- platform: speaker_source
name: ${friendly_name}
media_pipeline:
speaker: dac_speaker
sources: [airplay_audio]
sample_rate: 44100
num_channels: 1
For Music Assistant as well, the pack ships a second YAML with a Sendspin source next to airplay_audio on the same player.
Setup outline
- Wire the DAC/amp. Flash a speaker-only ESPHome config and play a local file from HA. Fix pins and brown-outs here, not after AirPlay is in the mix.
- Add PSRAM, the external component, the AirPlay source, and the
speaker_sourceplayer. Adopt the node. Confirm amedia_playerand theAirPlay activebinary. - From a phone on the same LAN, pick the AirPlay name. You should hear audio within a second or two;
AirPlay activeturns on; title/artist sensors update if the sender provides them. - Optional: add Sendspin, attach the player in Music Assistant, and confirm both sources can take the speaker (one at a time).
- Leave a 30-minute stream running. Drop counters should stay near zero on a quiet 2.4 GHz hop; raise
buffer_framesto 1024 if you seelate_frames/output_drops.
Troubleshooting
- Phone never sees the receiver. mDNS is filtered (guest Wi-Fi, AP client isolation, or a VLAN without Bonjour reflection). Put the phone and the ESP32 on the same LAN first; do not debug the DAC until the name appears.
- Name appears, no sound. I2S pins or
channeldo not match the amp (MAX98357A is mono). Confirm the speaker-only config still plays a HA file. - Stutters / late_frames climb. 512-frame buffer is tight on a busy radio, or the board has no PSRAM. Raise
buffer_frames, keep the node close to the AP, and do not run verbose logger on the audio build. - HA pause does nothing on the phone. Sender did not expose DACP. Local mute still works;
remote_controlcannot invent a control channel. - AirPlay 2 / multi-room is missing. Expected. Use Sendspin / Music Assistant for grouped ESPHome speakers, or a different firmware if you need AirPlay 2 sync.
- Compile fails on
media_source. ESPHome is older than 2026.9.1. Update the compiler; the native API this component uses is not in earlier releases.
Credits
Based on the community project ESPHome AirPlay: a native component that works as a media_source and the inventor7777/ESPHome-AirPlay component (itself a native-API rewrite of jptrsn/esphome-raop). This write-up is an original Super Mini / MAX98357A recipe, not a paste of those pages.