Objective and use case
What you’ll build: An I2S network audio player using a Raspberry Pi Zero 2 W and a PCM5102A DAC, capable of receiving uncompressed PCM audio over the network and outputting it via the I2S interface.
Why it matters / Use cases
- Stream high-quality audio from a network source to a local audio system, enhancing home audio setups.
- Utilize the Raspberry Pi Zero 2 W’s compact size for portable audio applications, such as in DIY speaker projects.
- Integrate with home automation systems to create a smart audio player that responds to network commands.
- Experiment with audio processing and streaming technologies in a hands-on project for educational purposes.
Expected outcome
- Achieve low-latency audio playback with less than 50 ms delay from network to output.
- Maintain audio quality with a signal-to-noise ratio (SNR) exceeding 100 dB.
- Support streaming audio at a minimum of 16-bit/44.1 kHz resolution.
- Handle multiple audio streams simultaneously with minimal packet loss (less than 1%).
Audience: Advanced users familiar with Linux, ALSA, and Python; Level: Advanced
Architecture/flow: The system architecture involves a Raspberry Pi Zero 2 W connected to a PCM5102A DAC via I2S, receiving audio data over UDP from a network source.
Advanced Practical Case: Raspberry Pi Zero 2 W + PCM5102A DAC as an I2S Network Audio Player
Objective: Build an I2S network audio player on a Raspberry Pi Zero 2 W using a PCM5102A DAC module. The player will receive uncompressed PCM audio over the network (UDP) and output it via the I2S interface to the DAC.
Target audience: Advanced users familiar with Linux, ALSA, and Python.
OS and language baseline:
– Raspberry Pi OS Bookworm 64‑bit
– Python 3.11
Prerequisites
- A Linux/macOS/Windows workstation to flash the microSD card.
- Local network with Wi‑Fi coverage (2.4 GHz) for the Raspberry Pi Zero 2 W.
- Basic soldering and wiring capability for I2S connections.
- An external audio amplifier or powered speakers to connect to the PCM5102A DAC outputs.
- Familiarity with the Linux terminal, SSH, and editing configuration files.
Materials (exact model)
- Device: Raspberry Pi Zero 2 W + PCM5102A DAC
- microSD card (≥16 GB, Class 10/UHS‑I recommended)
- 5 V / 2.5 A USB power supply for the Pi Zero 2 W
- PCM5102A I2S DAC breakout board (typical module with BCK, LRCK, DIN, GND, VCC pins; many accept 5 V input)
- Jumper wires (female‑female or mixed, per your board headers)
- RCA cables (or 3.5 mm cable) from DAC to amplifier/speakers
- Optional: small breadboard, 10 kΩ resistor, SPST push button (for later improvements)
Setup/Connection
1) Flash Raspberry Pi OS Bookworm 64‑bit
- Use Raspberry Pi Imager on your workstation.
- Device: Raspberry Pi Zero 2 W
- OS: Raspberry Pi OS (64‑bit) Bookworm
- Storage: your microSD card
- Click the gear icon to preconfigure:
- Set hostname (e.g., rpi‑i2s)
- Enable SSH (password or public key)
- Configure Wi‑Fi SSID, password, country
- Set locale, keyboard, timezone
- Write and verify.
Insert the card into the Pi Zero 2 W, connect power, and SSH into the device when it appears on the network. Verify Python and kernel:
ssh pi@rpi-i2s.local
uname -a
python3 --version
You should see Linux kernel 6.x and Python 3.11.x.
2) Wire the PCM5102A DAC to the Pi Zero 2 W (I2S)
The PCM5102A supports “3‑wire” I2S without MCLK (master clock). The Raspberry Pi’s I2S pins from the primary PCM/I2S controller are routed as follows.
Connection table:
| Function | Raspberry Pi Zero 2 W Pin (GPIO) | PCM5102A Pin (typical labels) |
|---|---|---|
| 3.3 V | Pin 1 (3V3) or 5 V Pin 2/4 | VCC (see module labeling) |
| GND | Pin 6 (GND) | GND |
| BCLK | Pin 12 (GPIO18, PCM_CLK) | BCK / BCLK |
| LRCLK | Pin 35 (GPIO19, PCM_FS) | LRCK / WS |
| DIN | Pin 40 (GPIO21, PCM_DOUT) | DIN |
Notes:
– Many PCM5102A boards accept 5 V on VCC and regulate internally. If your board is explicitly 3.3 V‑only, use Pi Pin 1 (3.3 V). Always confirm the exact marking on your module before powering.
– MCLK is not used on the Raspberry Pi with this overlay; don’t connect it even if present on the module.
– Keep I2S wires short and tidy to avoid signal integrity issues.
3) Enable I2S and the HifiBerry DAC overlay
On Raspberry Pi OS Bookworm the firmware config is under /boot/firmware/config.txt. Disable the on‑board analog audio and enable the PCM5102A‑compatible overlay:
sudo cp /boot/firmware/config.txt /boot/firmware/config.txt.bak
sudo nano /boot/firmware/config.txt
Add the following lines near the end:
dtparam=audio=off
# Enable I2S DAC compatible with PCM5102A
dtoverlay=hifiberry-dac
Save and reboot:
sudo reboot
After reboot, verify the ALSA card:
aplay -l
Expected output includes a card similar to:
- card 0: sndrpihifiberry [snd_rpi_hifiberry_dac], device 0: HiFiBerry DAC HiFi pcm5102a-hifi-0
If you don’t see it, see Troubleshooting.
4) Set a safe ALSA default with soft volume
Create /etc/asound.conf to define a default PCM with software volume control (the PCM5102A lacks a hardware mixer):
sudo nano /etc/asound.conf
Paste:
pcm.softvol {
type softvol
slave {
pcm "plughw:sndrpihifiberry"
}
control {
name "SoftMaster"
card "sndrpihifiberry"
}
min_dB -51.0
max_dB 0.0
}
pcm.!default {
type plug
slave.pcm "softvol"
}
ctl.!default {
type hw
card "sndrpihifiberry"
}
Test playback (sine tone):
speaker-test -D default -c 2 -t sine -f 1000 -l 1
You should hear a brief 1 kHz tone through your amplifier/speakers.
Full Code
We’ll implement a minimal, low‑latency UDP PCM16 network protocol with a jitter buffer on the Raspberry Pi. The sender (for validation) can run on your workstation. Audio format: stereo, 48 kHz, 16‑bit little‑endian, 10 ms frames. Each UDP packet contains:
– 8‑byte header: magic b’P16S’ (4) + sequence (uint32 BE)
– 1920‑byte payload: 480 frames × 2 channels × 2 bytes
Playback uses the ALSA default device via sounddevice (PortAudio), which will route to our softvol on the PCM5102A.
Receiver (run on the Raspberry Pi)
Create the project folder and receiver script:
mkdir -p ~/i2s-net-player
cd ~/i2s-net-player
nano net_i2s_receiver.py
Paste:
#!/usr/bin/env python3
import argparse
import asyncio
import collections
import logging
import socket
import struct
import sys
import time
import numpy as np
import sounddevice as sd
MAGIC = b'P16S'
SAMPLE_RATE = 48000
CHANNELS = 2
FRAME_SAMPLES = 480 # 10 ms @ 48 kHz
BYTES_PER_SAMPLE = 2 # int16
PAYLOAD_SIZE = FRAME_SAMPLES * CHANNELS * BYTES_PER_SAMPLE
HEADER_SIZE = 8 # MAGIC(4) + seq(4)
PACKET_SIZE = HEADER_SIZE + PAYLOAD_SIZE
class UdpAudioReceiver:
def __init__(self, host, port, device=None, prebuffer_ms=50, gain_db=0.0, rcvbuf=1 << 20):
self.host = host
self.port = port
self.device = device
self.prebuffer_frames = int((prebuffer_ms / 1000.0) * SAMPLE_RATE)
# We'll buffer by frames; each network packet is FRAME_SAMPLES frames.
self.prebuffer_packets = max(1, self.prebuffer_frames // FRAME_SAMPLES)
self.gain = 10 ** (gain_db / 20.0)
self.rcvbuf = rcvbuf
self.queue = asyncio.Queue(maxsize=256)
self.sock = None
self.expected_seq = None
self.drop_count = 0
self.start_playback = asyncio.Event()
self.play_started = False
def _open_socket(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.rcvbuf)
sock.bind((self.host, self.port))
self.sock = sock
logging.info("Listening on UDP %s:%d (SO_RCVBUF=%d)", self.host, self.port, self.rcvbuf)
async def _producer(self):
loop = asyncio.get_running_loop()
self._open_socket()
self.sock.setblocking(False)
buf = bytearray(PACKET_SIZE)
view = memoryview(buf)
buffered_packets = 0
while True:
try:
nbytes, _addr = await loop.sock_recvfrom(self.sock, PACKET_SIZE)
except asyncio.CancelledError:
break
if nbytes != PACKET_SIZE:
logging.warning("Bad packet size: got %d, expected %d", nbytes, PACKET_SIZE)
continue
if view[:4] != MAGIC:
logging.warning("Bad magic")
continue
seq = struct.unpack(">I", view[4:8])[0]
payload = bytes(view[8:])
# Track loss
if self.expected_seq is None:
self.expected_seq = (seq + 1) & 0xFFFFFFFF
else:
if seq != self.expected_seq:
self.drop_count += (seq - self.expected_seq) & 0xFFFFFFFF
self.expected_seq = (seq + 1) & 0xFFFFFFFF
else:
self.expected_seq = (self.expected_seq + 1) & 0xFFFFFFFF
# Convert to ndarray for post-processing if needed
frame = np.frombuffer(payload, dtype=np.int16).reshape(-1, CHANNELS).astype(np.float32)
frame *= self.gain
# clip
np.clip(frame, -32768.0, 32767.0, out=frame)
frame_i16 = frame.astype(np.int16)
try:
self.queue.put_nowait(frame_i16)
if not self.play_started:
buffered_packets += 1
if buffered_packets >= self.prebuffer_packets:
self.start_playback.set()
except asyncio.QueueFull:
# Drop oldest to prevent unbounded latency
_ = self.queue.get_nowait()
await self.queue.put(frame_i16)
def _sd_callback(self, outdata, frames, time_info, status):
if status:
logging.debug("SoundDevice status: %s", status)
needed = frames
out = np.zeros((frames, CHANNELS), dtype=np.int16)
filled = 0
while filled < needed:
try:
block = self.queue.get_nowait()
except asyncio.QueueEmpty:
break
# Each block is FRAME_SAMPLES frames
take = min(needed - filled, block.shape[0])
out[filled:filled + take, :] = block[:take, :]
filled += take
outdata[:] = out
async def run(self):
prod_task = asyncio.create_task(self._producer())
logging.info("Waiting for prebuffer: ~%d packets (~%d ms)", self.prebuffer_packets, int(self.prebuffer_packets * 10))
await self.start_playback.wait()
self.play_started = True
device_name = self.device if self.device else None
logging.info("Opening ALSA device: %s", device_name if device_name else "default")
with sd.OutputStream(
samplerate=SAMPLE_RATE,
channels=CHANNELS,
dtype='int16',
device=device_name,
callback=self._sd_callback,
blocksize=FRAME_SAMPLES,
latency='low',
):
logging.info("Playback started. Press Ctrl+C to stop.")
try:
while True:
await asyncio.sleep(1.0)
except asyncio.CancelledError:
pass
except KeyboardInterrupt:
pass
prod_task.cancel()
with contextlib.suppress(Exception):
await prod_task
def main():
parser = argparse.ArgumentParser(description="I2S Network Audio Receiver (PCM16 over UDP) for Raspberry Pi Zero 2 W + PCM5102A DAC")
parser.add_argument("--host", default="0.0.0.0", help="Listen address (default: 0.0.0.0)")
parser.add_argument("--port", type=int, default=5005, help="Listen UDP port (default: 5005)")
parser.add_argument("--device", default=None, help="ALSA/PortAudio device name or index (default: system default)")
parser.add_argument("--prebuffer-ms", type=int, default=50, help="Initial prebuffer in ms (default: 50)")
parser.add_argument("--gain-db", type=float, default=0.0, help="Digital gain in dB (default: 0.0)")
parser.add_argument("--rcvbuf", type=int, default=(1<<20), help="SO_RCVBUF bytes (default: 1 MiB)")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase verbosity")
args = parser.parse_args()
level = logging.WARNING - min(args.verbose, 2) * 10
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s: %(message)s")
try:
asyncio.run(UdpAudioReceiver(
host=args.host, port=args.port, device=args.device,
prebuffer_ms=args.prebuffer_ms, gain_db=args.gain_db, rcvbuf=args.rcvbuf
).run())
except KeyboardInterrupt:
pass
if __name__ == "__main__":
import contextlib
main()
Sender (run on your workstation for validation)
This sender reads a WAV file (stereo, 48 kHz, 16‑bit LE preferred) and streams it as 10 ms UDP packets to the Pi.
nano send_pcm16_udp.py
Paste:
#!/usr/bin/env python3
import argparse
import socket
import struct
import time
import numpy as np
import soundfile as sf
MAGIC = b'P16S'
SAMPLE_RATE = 48000
CHANNELS = 2
FRAME_SAMPLES = 480 # 10 ms
BYTES_PER_SAMPLE = 2
def main():
p = argparse.ArgumentParser(description="Send PCM16 UDP stream for i2s-network-audio-player")
p.add_argument("wav", help="Path to stereo 48 kHz 16-bit WAV")
p.add_argument("--ip", required=True, help="Receiver IP")
p.add_argument("--port", type=int, default=5005, help="Receiver UDP port (default: 5005)")
p.add_argument("--repeat", action="store_true", help="Loop playback")
p.add_argument("--throttle", action="store_true", help="Throttle to realtime (default: True)", default=True)
args = p.parse_args()
data, sr = sf.read(args.wav, dtype='int16', always_2d=True)
if sr != SAMPLE_RATE or data.shape[1] != CHANNELS:
raise SystemExit(f"Input must be {SAMPLE_RATE} Hz, stereo. Got {sr} Hz, {data.shape[1]} ch")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
seq = 0
frame_bytes = FRAME_SAMPLES * CHANNELS * BYTES_PER_SAMPLE
cursor = 0
t0 = time.perf_counter()
while True:
if cursor + FRAME_SAMPLES > data.shape[0]:
if args.repeat:
cursor = 0
t0 = time.perf_counter()
else:
break
frame = data[cursor:cursor + FRAME_SAMPLES, :]
payload = frame.astype(np.int16).tobytes()
packet = MAGIC + struct.pack(">I", seq) + payload
sock.sendto(packet, (args.ip, args.port))
seq = (seq + 1) & 0xFFFFFFFF
cursor += FRAME_SAMPLES
if args.throttle:
# Aim for real-time pacing: 10 ms per packet
time.sleep(0.010)
if __name__ == "__main__":
main()
Build/Flash/Run commands
All commands below run on the Raspberry Pi unless otherwise specified.
System update and base packages
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y python3-venv python3-dev git build-essential \
alsa-utils libasound2-dev libportaudio2 portaudio19-dev \
libsndfile1 \
iperf3
Optional but recommended: disable Wi‑Fi power saving (to reduce dropouts):
sudo mkdir -p /etc/NetworkManager/conf.d
echo -e "[connection]\nwifi.powersave = 2" | sudo tee /etc/NetworkManager/conf.d/wifi-powersave-off.conf
sudo systemctl restart NetworkManager
Python 3.11 virtual environment and packages
cd ~/i2s-net-player
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install numpy sounddevice soundfile
# Per assignment requirement (not strictly needed by the core demo):
pip install gpiozero smbus2 spidev
List sound devices to locate the DAC (optional):
python -c "import sounddevice as sd; print(sd.query_devices())"
You should see a device referencing sndrpihifiberry or default ALSA mapping.
Run the receiver
cd ~/i2s-net-player
source .venv/bin/activate
python net_i2s_receiver.py --host 0.0.0.0 --port 5005 --prebuffer-ms 50 -v
The receiver now waits for UDP audio.
Prepare the sender (on your workstation)
Install Python dependencies:
python3 -m pip install --user soundfile numpy
Find the Pi’s IP (on the Pi):
hostname -I
Assume it prints 192.168.1.50. On your workstation, run:
python3 send_pcm16_udp.py /path/to/stereo_48k_16bit.wav --ip 192.168.1.50 --port 5005 --repeat
If you lack a suitable WAV, create a test file (Linux/macOS with ffmpeg):
ffmpeg -f lavfi -i "sine=frequency=1000:duration=5" -ac 2 -ar 48000 -sample_fmt s16 test_1k_stereo.wav
Step‑by‑step Validation
1) Confirm the I2S DAC is recognized by ALSA:
– aplay -l should list snd_rpi_hifiberry_dac.
– Check dmesg for overlay load:
dmesg | grep -i -E "snd|hifiberry|pcm5102"
Expected: lines indicating pcm5102a codec and machine driver probed.
2) Validate basic audio path:
– Speaker test via ALSA default:
speaker-test -D default -c 2 -t sine -f 440 -l 1
Hear a brief tone. If silent, verify connections and amplifier input.
3) Validate ALSA naming and default route:
– List ALSA logical devices:
aplay -L | sed -n '1,120p'
Ensure default maps to softvol and underlying card is sndrpihifiberry.
4) Confirm Python can access the sound device:
– From the venv:
python - <<'PY'
import sounddevice as sd
for i, d in enumerate(sd.query_devices()):
print(i, d['name'], d['hostapi'], d['max_output_channels'])
PY
Identify an output device with ≥2 channels.
5) Network sanity test:
– Test Wi‑Fi throughput/quality between workstation and Pi:
– On Pi:
iperf3 -s
– On workstation:
iperf3 -c 192.168.1.50
– Expect at least several Mbit/s; our uncompressed stream is ~1.536 Mbit/s (48k × 16‑bit × 2 ch).
6) Run the full streaming path:
– Start receiver on the Pi:
cd ~/i2s-net-player
source .venv/bin/activate
python net_i2s_receiver.py --host 0.0.0.0 --port 5005 --prebuffer-ms 50 -v
You should see “Listening on UDP …” and then “Playback started” after a short prebuffer.
– Start sender on workstation:
python3 send_pcm16_udp.py test_1k_stereo.wav --ip 192.168.1.50 --port 5005 --repeat
The tone should play continuously. Replace with music WAV to validate musical playback.
7) Latency and stability checks:
– Adjust prebuffer for dropouts:
– Increase to 80–120 ms:
python net_i2s_receiver.py --prebuffer-ms 100
– If dropouts persist, confirm Wi‑Fi power saving is off and check RSSI quality (iw dev wlan0 link).
8) Volume control:
– Use softvol control:
alsamixer
– Select “SoftMaster” and adjust volume.
– Note: PCM5102A has no hardware volume; all control is digital.
Troubleshooting
- No sndrpihifiberry device in aplay -l:
- Ensure /boot/firmware/config.txt contains both:
- dtparam=audio=off
- dtoverlay=hifiberry-dac
- Reboot and recheck:
sudo reboot
aplay -l -
Inspect kernel messages:
dmesg | grep -i asoc
dmesg | grep -i hifiberry -
No audio, but device is present:
- Verify wiring:
- GPIO18 -> BCLK
- GPIO19 -> LRCLK/WS
- GPIO21 -> DIN
- Ground shared
- Correct VCC (5 V or 3.3 V matching your module)
- Test with speaker-test:
speaker-test -D default -c 2 -t sine -f 1000 - Ensure your amplifier input is correct and volume is up.
-
Check that the module’s output uses the right connectors (RCA/3.5 mm).
-
Distorted or choppy audio:
- Increase jitter buffer:
python net_i2s_receiver.py --prebuffer-ms 120 - Disable Wi‑Fi power saving:
nmcli radio wifi on
sudo iw dev wlan0 set power_save off
Persist via NetworkManager conf as shown earlier. - Reduce CPU load:
- Close other applications.
- Monitor:
top
vcgencmd measure_temp
-
Improve UDP buffering:
- Increase SO_RCVBUF (already defaulted to 1 MiB). You can raise:
sudo sysctl -w net.core.rmem_max=4194304
Then run the receiver with –rcvbuf 4194304.
- Increase SO_RCVBUF (already defaulted to 1 MiB). You can raise:
-
The Python receiver can’t open the sound device:
- List devices with sounddevice and choose an explicit device:
python net_i2s_receiver.py --device "default"
or
python net_i2s_receiver.py --device "sysdefault" -
Ensure libportaudio2 and portaudio19-dev are installed, and that sounddevice is installed in the venv.
-
Sender complains about sample rate/channels:
-
Convert your file to stereo 48 kHz 16‑bit:
ffmpeg -i input.any -ac 2 -ar 48000 -sample_fmt s16 out_48k_16bit_stereo.wav -
High noise floor or hum:
- Use a clean 5 V power supply.
- Keep I2S wires short and away from high‑current cables.
- Use shielded RCA cables to the amplifier.
Improvements
- Compression for bandwidth efficiency:
- Replace raw PCM with Opus or FLAC over RTP. Use GStreamer on both ends:
- Receiver example (on Pi):
gst-launch-1.0 udpsrc port=5006 caps="application/x-rtp, media=audio, encoding-name=OPUS, payload=96" \
! rtpopusdepay ! opusdec ! audioconvert ! audioresample ! alsasink device=default sync=true - Sender example:
gst-launch-1.0 filesrc location=music.wav ! wavparse ! audioconvert ! audioresample \
! opusenc bitrate=128000 ! rtpopuspay ! udpsink host=192.168.1.50 port=5006
- Receiver example (on Pi):
- AirPlay (RAOP) compatibility:
-
Use shairport-sync to turn the Pi into an AirPlay endpoint that outputs via I2S:
sudo apt install -y shairport-sync
sudo systemctl enable --now shairport-sync
Ensure default ALSA routes to your PCM5102A softvol. -
Multiroom sync:
-
Use Snapcast client/server for synchronized multiroom streaming over the network.
-
System service for auto‑start:
-
Create a systemd unit to start the Python receiver at boot:
sudo nano /etc/systemd/system/i2s-net-receiver.service
Contents:
«`
[Unit]
Description=I2S Network Audio Receiver (PCM16 UDP)
After=network-online.target
Wants=network-online.target[Service]
User=pi
WorkingDirectory=/home/pi/i2s-net-player
ExecStart=/home/pi/i2s-net-player/.venv/bin/python /home/pi/i2s-net-player/net_i2s_receiver.py –host 0.0.0.0 –port 5005 –prebuffer-ms 80
Restart=always
RestartSec=2[Install]
WantedBy=multi-user.target
Enable:
sudo systemctl daemon-reload
sudo systemctl enable –now i2s-net-receiver.service
«` -
Hardware controls:
-
Add GPIO buttons via gpiozero for play/pause or mute, and expose a simple HTTP API using Flask for remote control.
-
Better resampling and dithering:
-
Accept arbitrary input formats at the sender and resample to 48 kHz with high‑quality SRC before sending.
-
Monitoring:
- Add Prometheus metrics for packet loss, jitter buffer level, and CPU usage.
Final Checklist
- OS and language:
- Raspberry Pi OS Bookworm 64‑bit installed and updated.
- Python 3.11 available.
- Hardware:
- Raspberry Pi Zero 2 W powered by 5 V/2.5 A supply.
- PCM5102A DAC wired correctly:
- GPIO18 → BCLK
- GPIO19 → LRCLK/WS
- GPIO21 → DIN
- GND ↔ GND
- VCC per module spec (5 V or 3.3 V)
- RCA/3.5 mm outputs connected to amplifier/speakers.
- Firmware and ALSA:
- /boot/firmware/config.txt with:
- dtparam=audio=off
- dtoverlay=hifiberry-dac
- /etc/asound.conf softvol mapping to sndrpihifiberry
- speaker-test emits tone correctly.
- Python environment:
- ~/i2s-net-player/.venv created.
- pip installed: numpy, sounddevice, soundfile (and gpiozero, smbus2, spidev per requirement).
- Network:
- Receiver running: net_i2s_receiver.py on UDP port 5005.
- Sender running on workstation: send_pcm16_udp.py streaming a 48 kHz stereo WAV.
- Audio plays without dropouts; prebuffer adjusted as needed.
- Optional optimizations:
- Wi‑Fi power save disabled.
- Systemd unit installed for auto‑start.
With these steps completed, your Raspberry Pi Zero 2 W + PCM5102A DAC functions as a robust I2S network audio player, receiving PCM over UDP and outputting high‑quality audio via the I2S interface.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



