Internet radio
An MP3 station over Wi-Fi, decoded on the ESP32-S3 and played in stereo on the TK103. Two libraries do the work: arduino-audio-tools moves the audio from the network to the DAC, and arduino-libhelix decodes the MP3. Type a number from 0 to 1 in the Serial Monitor to set the volume.
Install the two libraries
Neither library is in the ESP32 core. From GitHub, download each as a ZIP:
- pschatzmann/arduino-audio-tools, which moves audio between streams.
- pschatzmann/arduino-libhelix, the MP3 decoder.
Then in the Arduino IDE, Sketch > Include Library > Add .ZIP Library, once for each file. The sketch was compiled with arduino-audio-tools 1.2.6 and arduino-libhelix 0.9.4; a much newer release may have moved a header.
Wire it
The same six wires as Left, then right: GND, 3V3, BCK to GPIO 42, DIN to 41, LRCK to 40, and XSMT to 39 if you like. Headphones or a powered speaker in the jack.
Your network and the station
Change your-network and your-password to your Wi-Fi's name and password.
STATION is Radio Swiss Jazz's public MP3 stream, the one the library uses in
its own example; it answered on 2026-09-24. Any http MP3 stream works in its
place. An https one needs more memory, and is not where to start.
How the sketch moves sound
Each object hands its output to the next. copier.copy() moves one buffer
through the whole chain, and loop() calls it as fast as it can, so the DAC
keeps playing while the next buffer arrives.
The DAC starts at 44.1 kHz, and mp3.begin() lets the decoder correct that to
the station's real rate once it has read the stream. Most MP3 streams are
44.1 kHz stereo; at 16-bit that is a bit clock of 1.4112 MHz, 32 fs, inside
TI's table.
Volume
The sketch starts at 0.3. Open the Serial Monitor at 115200, leave its line ending on Newline, and type a number from 0 to 1. 0 is silent and 1 is full scale; VolumeStream spaces the steps between like a volume knob rather than in a straight line. The sketch prints the new volume back.
The sketch reads the whole line and trims the line ending off it, so a blank line, or a number outside 0 to 1, leaves the volume where it was.
The code
Put your network's name and password in SSID and PASSWORD. URLStream fetches the station, the MP3 decoder turns it into 16-bit stereo samples, VolumeStream scales them, and I2SStream sends them to the TK103. loop() moves one buffer per call and reads a volume from the Serial Monitor.
// TK103 PCM5102 DAC: internet radio in stereo, from an MP3 stream.
//
// Wiring, TK103 to ESP32-S3:
// GND -> GND, 3V3 -> 3V3
// BCK -> GPIO 42, DIN -> GPIO 41, LRCK -> GPIO 40, XSMT -> GPIO 39
// Headphones or powered speakers in the jack.
//
// Libraries (Sketch > Include Library > Add .ZIP Library), from GitHub:
// pschatzmann/arduino-audio-tools and pschatzmann/arduino-libhelix
// Arduino IDE: Tools > Board > "ESP32S3 Dev Module" (esp32 core 3.x),
// USB CDC On Boot: Enabled.
// Type a number from 0 to 1 in the Serial Monitor (115200) for volume.
#include "AudioTools.h"
#include "AudioTools/AudioCodecs/CodecMP3Helix.h"
#include "AudioTools/Communication/AudioHttp.h"
const char *SSID = "your-network";
const char *PASSWORD = "your-password";
const char *STATION = "http://stream.srg-ssr.ch/m/rsj/mp3_128";
const int PIN_BCK = 42, PIN_DIN = 41, PIN_LRCK = 40, PIN_XSMT = 39;
URLStream url(SSID, PASSWORD); // bytes from the station
I2SStream dac; // samples to the TK103
VolumeStream volume(dac); // scales every sample
EncodedAudioStream mp3(&volume, new MP3DecoderHelix());
StreamCopy copier(mp3, url); // moves one buffer per call
void setup() {
Serial.begin(115200);
AudioToolsLogger.begin(Serial, AudioToolsLogLevel::Warning);
pinMode(PIN_XSMT, OUTPUT);
digitalWrite(PIN_XSMT, HIGH);
auto cfg = dac.defaultConfig(TX_MODE);
cfg.pin_bck = PIN_BCK;
cfg.pin_ws = PIN_LRCK;
cfg.pin_data = PIN_DIN;
cfg.sample_rate = 44100; // the decoder corrects this
cfg.channels = 2;
cfg.bits_per_sample = 16;
dac.begin(cfg);
volume.begin(cfg);
volume.setVolume(0.3);
mp3.begin(); // tells the DAC the real rate
url.begin(STATION, "audio/mp3");
}
void loop() {
copier.copy();
if (Serial.available()) {
String line = Serial.readStringUntil('\n');
line.trim(); // drops the line ending
float v = line.toFloat();
if (line.length() > 0 && v >= 0 && v <= 1) {
volume.setVolume(v);
Serial.printf("volume %.2f\n", v);
}
}
}Compiled, not run, with esp32 core 3.3.10, arduino-audio-tools 1.2.6 and arduino-libhelix 0.9.4: 1,111,497 bytes, 84 % of the default app partition, so no partition change is needed. Its tested ancestor is the owner's bench player, which played an MP3 from flash through the same two libraries at 44.1 kHz on a TK103. Leave the Serial Monitor's line ending on Newline, its default, when typing a volume.
When it does not work
The Serial Monitor is set to No line ending, so the sketch waits a second for the end of the line before it gives up and reads the number. Set the line ending to Newline, the default, and the change is immediate.
The libraries are not installed, or only one is. Add both ZIPs with Sketch > Include Library > Add .ZIP Library: arduino-audio-tools and arduino-libhelix, from pschatzmann's GitHub pages. Then restart the IDE and compile again.
Usually the network: check SSID and PASSWORD, and that the board is in range. If it connects and still plays nothing, the station may be down: try it in a browser on the same network, or put another http MP3 stream in STATION.
The DAC plays what the network delivered, and a weak Wi-Fi signal delivers it late. Move the board nearer the router.
The sketch is written for http. An https stream needs the ESP32-S3 to run TLS as well, which needs more memory; start with an http stream, such as the one in the sketch.
Six causes, the red LED to sort them, and where to look first.
When it stays silent →Edit this page — content/books/i2s-dac/internet-radio.mdx
Questions about this product
See what other owners have asked, and read their solutions.
I2S DAC
Loading discussions…
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.