With the rise of WLED and other smart WiFi enabled LED controllers that have built-in support for receiving E1.31 or DDP data over WiFi, it's becoming practical to run a small holiday light show entirely from a single Raspberry Pi. Instead of needing dedicated show controllers and proprietary hardware, a few WLED-flashed ESP32 devices and a Pi running Falcon Player can drive a small neighborhood display — sequences, audio, and pixel control all from one board.
SBS+ takes this a step further: not only does the Pi run the show, it also serves as the WiFi access point for your smart devices and provides synced audio to your phone and your audience's phones — no internet connection, no cloud services, no extra hardware beyond what you already have. All the wonderful things FPP can do but now coupled with the ability to play audio from your phone/device. We even take a stab at correcting for Bluetooth delay with an offset setting.
SBS+ combines two tools into one install that runs on a single Raspberry Pi:
| Component | What it does |
|---|---|
| FPP Admin Control | Show-owner control page — start/stop sequences and playlists, hear synced show audio, BT speaker delay calibration. Runs on the admin AP (wlan0). |
| FPP Phone Listener | Public audience page — phones connect to an open WiFi AP (wlan1), get auto-redirected by captive portal, and hear synced show audio. No password, no app, no setup. |
Both pages share the same WebSocket sync server and adaptive PLL engine, so the admin phone and every audience phone stay locked to FPP's playback position.
Tested on FPP 9.4 (Raspberry Pi OS Bookworm). Should work on any FPP version that uses
/opt/fpp/www/as its web root (FPP 8+).
The admin page is for the show owner. It gives you:
- Playback control — start/stop playlists and sequences from your phone
- Synced show audio — hear the show audio in real time, synced to FPP via adaptive PLL
- Clock management — detects when the Pi's clock is wrong (no RTC/NTP) and offers to set it from your phone
- BT speaker delay — select a Bluetooth delay profile to compensate for BT speaker latency, with a link to the calibration page
- Auto-detection — picks up sequences started from any source (FPP web UI, scheduler, API)
- Debug tools — live PLL state, clock offset, error history, playback rate, and client sync log
The listener page is for audience members. It provides:
- Synced show audio — phone plays the show audio in sync with FPP (same PLL as admin)
- Now Playing display — shows the current track name
- Zero setup — phones connect to the open WiFi AP and are auto-redirected by captive portal
- QR code compatible — works with QR codes generated by fpp-listener-sync
- BT speaker delay — same delay profile support as admin page
- Debug tools — same PLL diagnostics as admin (for troubleshooting)
The FPP plugin dashboard replaces the default network configuration page and provides:
- Network interface manager — card-based UI showing each detected interface (wlan0, wlan1, eth0) with a role selector (SBS, Listener, Show Network, Unused)
- Per-interface config — SSID, channel, password, IP address for each AP role
- Quick links — admin page, listener page, QR code generator, printable sign
- Connected clients — MAC address, IP, hostname, signal strength, and connection time for each device on the AP
- Logs & diagnostics — view logs from ws-sync, listener-ap, dnsmasq, or sync reports with selectable line counts
- Self-test — checks all services, ports, interfaces, and firewall rules
- Service restart — restart AP, WebSocket sync, or DNS/DHCP individually
- FPP integration — registers a header icon and footer button in FPP's web interface for quick access
For Bluetooth speakers connected to devices that are streaming audio from the Pi, audio arrives with a delay (typically 50-300ms). The calibration page helps measure and compensate for this:
- Test sequence generator — select output channels and generate a
_bt_cal.fseqthat flashes lights every second - Calibration runner — start the test sequence and adjust the delay slider (0-500ms) until Web Audio clicks are in sync with the light flashes
- Delay profiles — save named profiles (stored in browser localStorage) and select them from admin.html or listen.html
- RPi Bluetooth control — scan for BT devices, pair, connect/disconnect, and adjust volume — all from the calibration page
| Mode | What it means | Hardware |
|---|---|---|
| SBS (Single Board Show) | One AP for admin + E1.31 show devices. No audience listener. | Pi + 1 WiFi radio (onboard or USB) |
| SBS+ (Single Board Show Plus) | SBS + adds a second AP for audience phones. Two isolated networks on one Pi. | Pi + 2 WiFi radios |
SBS+ runs two WiFi access points on one Raspberry Pi — one for the show owner, one for the audience:
| AP | Role | SSID (default) | Security | Purpose |
|---|---|---|---|---|
| Admin/SBS | sbs |
EAVESDROP |
WPA2 | Admin control page + E1.31 show devices (WLED bulbs, controllers). Default IP: 192.168.40.1 |
| Phone Listener | listener |
SHOW_AUDIO |
Open | Audience phones hear synced show audio via captive portal. Default IP: 192.168.50.1 |
How it works:
- The show owner connects to EAVESDROP (WPA2, default password
Listen123) and opensadmin.htmlto control the show - Audience phones connect to SHOW_AUDIO (open WiFi) and are automatically redirected to the listen page via captive portal
- The two networks are fully isolated — phones on SHOW_AUDIO cannot reach admin pages, FPP settings, or E1.31 devices on the admin network
- Both APs share the same WebSocket sync server, so all clients stay in sync
- Ethernet remains the show backbone for E1.31/multisync to remote FPP players
To enable SBS+ mode:
- Plug in a USB WiFi adapter (for wlan1)
- Open the plugin dashboard (Status > SBS Audio Sync) and assign roles to each interface
- Save & Restart
In SBS mode (no USB adapter): Only one interface runs. The admin page and E1.31 devices share the same AP. The public listener page is still accessible but there's no separate audience AP.
The plugin is interface-agnostic — it reads roles.json and configures whatever wlan* interfaces are present. You can use any combination of onboard and USB WiFi:
| Setup | Interfaces | Best for |
|---|---|---|
| Onboard + USB | wlan0 (onboard) = SBS, wlan1 (USB) = Listener | Most common. Simple, one adapter to buy. |
| Dual USB | wlan0 + wlan1 (both USB) | Best range and reliability. Disable onboard WiFi with dtoverlay=disable-wifi in /boot/config.txt. Frees the shared onboard radio for Bluetooth. |
| Onboard only | wlan0 (onboard) = SBS | SBS mode only (no audience AP). |
For the Listener AP (audience phones), a USB adapter with an external antenna is recommended — it handles more concurrent clients and provides better range across a yard. The Pi's onboard BCM43455 (brcmfmac) is limited to ~10-15 clients and shares its radio with Bluetooth.
Recommended USB chipsets (good Linux AP support):
- RTL8812AU — dual-band 2.4/5 GHz, external antenna, excellent AP mode
- RT5370 — 2.4 GHz, compact, very reliable AP mode, well-supported on Pi
If you previously used fpp-listener-sync and generated QR codes with its qrcode.html page, those QR codes will continue to work. They point to http://<AP_IP>/listen/ which redirects to listen.html — the public listen page.
To access the admin page directly, navigate to http://<AP_IP>/listen/admin.html.
The Raspberry Pi 3B has no real-time clock (RTC) battery and typically relies on NTP for time sync. When running as a standalone AP (no internet connection), the Pi's clock resets to the OS image date on every reboot. This causes sync problems because the WebSocket server timestamps are wrong.
SBS+ handles this automatically:
-
Admin page (Option B): When the admin page connects and detects the Pi's clock is off by more than 60 seconds, it shows a warning banner: "Pi clock is wrong — off by ~X days. Set it from your phone?" Tapping Yes sends the phone's current time to the server, which sets the Pi's system clock. The
ws-syncservice hasCAP_SYS_TIMEcapability so it can set the clock without root. -
Both pages (Option A — automatic backup): The sync math compensates for clock mismatch by using the measured clock offset in all timestamp comparisons. Even if the Pi's clock is days off, sync still works because the PLL uses offset-adjusted timestamps. Messages older than 2 seconds (real elapsed) are silently discarded.
If this tool saved you time or made your show better, consider buying me a coffee or donate for me to get more tokens:
SBS+ must be installed on your master FPP (the one in player mode), not on a remote. It needs:
- Direct access to the music files in
/home/fpp/media/music/(both Admin Control and Phone Listener serve audio from here) - The FPP API at
127.0.0.1to read playback status and start/stop playlists - Remotes don't have to store media locally — they only receive channel data from the master. Typically, there is no media to play on a remote
- SBS mode: No USB WiFi adapter needed — admin AP runs on onboard wlan0
- SBS+ mode: Plug in a USB WiFi adapter for wlan1 (the audience listener AP)
If your master controls the show, that's where this goes.
Before you start, make sure you have:
- A Raspberry Pi running Falcon Player (FPP) 8.x+ in player (master) mode
- Your FPP already has sequences (.fseq files) and matching audio files (.mp3) loaded
- A computer on the same network as your FPP to run the install commands
You need to get a command line on your FPP. Pick one of these methods:
Option A — SSH from your computer:
-
Windows: Open PowerShell or Command Prompt and type:
ssh fpp@YOUR_FPP_IPReplace
YOUR_FPP_IPwith your FPP's IP address (for example10.1.66.204). When it asks for a password, typefalconand press Enter. -
Mac/Linux: Open Terminal and type the same command above.
Option B — Use the FPP web interface:
- Open your browser and go to
http://YOUR_FPP_IP/ - Click on Content Setup in the menu
- Click File Manager
- (This method only works for uploading files — you'll still need SSH for the install command)
Once you're logged in via SSH, type these commands one at a time (press Enter after each one):
cd /home/fppgit clone https://github.com/UndocEng/fpp-sbs-plus.gitcd fpp-sbs-plussudo ./install.shThe installer runs 16 steps including web file deployment, service installation, Apache configuration, plugin registration, and self-tests. On success you'll see:
=========================================
Install successful! v4.3.1
=========================================
FPP Dashboard: Status > SBS Audio Sync
Admin: http://YOUR_FPP_IP/listen/admin.html
Sync: WebSocket (ws://YOUR_FPP_IP/ws)
SBS AP: EAVESDROP (WPA2) on wlan0 (192.168.40.1)
=========================================
If a USB WiFi adapter is detected and assigned as a listener, it also shows:
Listen: SHOW_AUDIO (open) on wlan1 (192.168.50.1)
Public: http://192.168.50.1/listen/
- On your phone (connected to the same network as your FPP), open the browser
- Go to
http://YOUR_FPP_IP/listen/ - You should see the Show Audio page with a Playback section at the top
- Select a sequence from the dropdown and tap Start — your lights and audio should begin
- Tap anywhere on the page to unlock audio (required by mobile browsers on first visit)
That's it! You're done.
- Connect your phone to the EAVESDROP WiFi (default password:
Listen123) - Open
http://192.168.40.1/listen/admin.html(or the AP IP you configured) - Pick a playlist or sequence from the dropdown and tap Start
- Audio plays through your phone speaker while lights run on your E1.31 devices
- If you see a "Pi clock is wrong" banner, tap Yes to set the clock from your phone
- Audience members connect to the SHOW_AUDIO WiFi (open, no password)
- Their phone's captive portal auto-redirects to the listen page
- Audio starts syncing automatically when a sequence is playing
- No interaction needed — it just works
Both pages automatically detect when a sequence is playing — even if started from FPP's web UI, the scheduler, or any other source. Just keep the page open and it will start syncing as soon as something plays.
Tap the Stop button on the admin page.
Both pages have a Debug checkbox at the bottom of the sync card to see live diagnostics: transport type, clock offset, PLL state, error history, and playback rate. The Client Log checkbox shows a running log of sync events. On the admin page, enabling Server Log sends telemetry to the Pi for analysis via sync.log.
- On iPhone, check that the ringer switch (on the side of the phone) is not on silent
- Turn up the volume on the phone
- Tap anywhere on the page — mobile browsers require a user gesture before they'll play audio
- Check that you have
.mp3files in/home/fpp/media/music/with the same name as your.fseqfiles (e.g.Elvis.fseqneedsElvis.mp3)
This is an FPP output configuration issue, not a listener issue. Check:
- Go to your FPP web interface (
http://YOUR_FPP_IP/) - Click Input/Output Setup > Channel Outputs
- Make sure your output universes are active (checkbox enabled)
- Make sure the output IP addresses are correct (not your FPP's own IP)
- Enable the Debug checkbox and watch the PLL converge — it takes ~12-14 seconds after a track starts
- If the admin page shows a "Pi clock is wrong" banner, tap Yes to set the clock from your phone — sync math compensates automatically, but setting the correct time helps
- The Avg Error (2s) field should hover near 0 once locked — typical steady-state is 5-25ms
- After a Pi reboot with no internet, the clock resets — open the admin page first to fix the clock before starting a show
- Check the ws-sync service:
sudo systemctl status ws-sync - View logs:
journalctl -u ws-sync -f - Test the port:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/— should return426 - The page will automatically fall back to HTTP polling if WebSocket is unavailable
To get the latest version:
cd /home/fpp/fpp-sbs-plus
git pull
sudo ./install.shTo completely remove everything this project installed:
cd /home/fpp/fpp-sbs-plus
sudo ./uninstall.shThis removes:
- All web files from
/opt/fpp/www/listen/ - The
/musicsymlink - The
ws-syncWebSocket service (stopped and disabled) - The
listener-apWiFi AP service (stopped, hostapd/dnsmasq killed for both admin and show APs) - The Apache WebSocket proxy configuration and SBS+ rewrite rules from the FPP VirtualHost
- The sudoers entry for WiFi management
- The config directory at
/home/fpp/listen-sync/(AP config, hostapd config, scripts, sync log) - The FPP plugin registration and header icon
- The SBS+ footer button from
custom.js - Network routing rules (nftables tables
listener_apandshow_ap, policy routes) - Captive portal
.htaccessfrom the web root
After uninstalling, your FPP is exactly as it was before. You can then delete the project folder:
rm -rf /home/fpp/fpp-sbs-plus| File | Component | What it does |
|---|---|---|
www/listen/admin.html |
Admin Control | Admin page — playback controls, synced audio, BT delay profiles, clock management, debug UI |
www/listen/listen.html |
Phone Listener | Public listen page — audio sync, now playing display, BT delay profiles, debug UI |
www/listen/calibrate.html |
BT Calibration | BT delay calibration — FSEQ generator, delay slider, profiles, BT device pairing/volume |
www/listen/index.html |
Both | Redirects to listen.html (makes QR codes and captive portal work) |
www/listen/status.php |
Sync | Returns current FPP playback status as JSON (HTTP polling fallback) |
www/listen/admin.php |
Admin Backend | Start/stop commands, BT device management, calibration FSEQ generation |
www/listen/version.php |
Both | Returns version info (reads from VERSION file) |
www/listen/portal-api.php |
Phone Listener | Captive portal API (RFC 8908) — tells phones where to redirect |
www/listen/detect.php |
Phone Listener | Legacy captive portal detection fallback for older devices |
www/listen/logo.png |
Admin Control | Admin logo (gold/amber) |
www/listen/logo-public.png |
Phone Listener | Listener logo (blue/cyan) |
www/qrcode.html |
Utility | QR code generator for listener page URL |
www/music.php |
Utility | Audio file server with CORS headers for direct streaming |
| File | What it does |
|---|---|
server/ws-sync-server.py |
Python WebSocket server — polls FPP API every 200ms, broadcasts state to all clients, handles clock sync pings, receives telemetry reports, and sets Pi clock from admin page |
server/listener-ap.sh |
Brings up admin AP on wlan0 (with power save disabled), optionally show AP on wlan1 (SBS+), configures nftables isolation and captive portal |
server/listener-ap.service |
Systemd service for the WiFi access point(s) |
| File | What it does |
|---|---|
config/ws-sync.service |
Systemd service for the WebSocket server — runs as fpp user with CAP_SYS_TIME (for clock set), 64MB RAM / 25% CPU limits |
config/apache-listener.conf |
Apache config — proxies /ws on port 80 to WebSocket server on port 8080 |
config/ap.conf |
Default AP config template (admin AP interface, IP, SBS+ settings) |
config/hostapd-show.conf |
Default hostapd config for SBS+ public AP (open WiFi, ap_isolate=1) |
www/.htaccess-show |
Captive portal rewrite template for SBS+ show AP — redirects all HTTP to listen.html, blocks admin.html |
| File | What it does |
|---|---|
plugin.php |
Plugin dashboard — card-based network interface manager, connected clients, logs, self-test |
listener-api.php |
Dashboard backend — interface config, role management, service control, client scanning |
api.php |
FPP plugin API — header indicator icon (gold headphones) linking to admin page |
pluginInfo.json |
FPP plugin metadata for plugin manager registration |
fpp-network.php |
Wrapper to load FPP's original networkconfig.php (backed up during install) |
install.sh |
Installs everything — web files, services, AP, plugin, sudoers, Apache config, CRLF fixes |
uninstall.sh |
Removes everything — restores FPP to original state |
SBS+ uses an adaptive Phase-Locked Loop (PLL) to keep the phone's audio in sync with FPP's sequence playback. Instead of repeatedly jumping to the correct position (which causes audible pops), it smoothly adjusts the playback speed to converge on FPP's position and stay locked.
The WebSocket server (ws-sync-server.py) polls FPP's API every 200ms and broadcasts state to all connected clients concurrently. The browser connects via WebSocket (proxied through Apache on port 80 at /ws), with automatic HTTP polling fallback if WebSocket is unavailable.
NTP-style clock offset estimation uses ping/pong round-trips through the WebSocket, with a median filter + EWMA for stable offset calculation.
The sync engine runs through three phases:
1. Start (first message after a track begins)
- Preloads the audio file and waits for metadata
- Seeks to FPP's current position
- Starts playback, enters 1.5-second settle period
2. Calibrate (~800ms minimum, 6+ samples)
- Collects
{local_time, fpp_position}pairs - Computes a least-squares linear regression to find the rate ratio between FPP's clock and the phone's clock
- Clamps the base rate to +/-1% (rejects garbage calibration)
3. Locked (ongoing)
- Computes phase error:
fpp_position - audio.currentTime - Maintains a 2-second rolling average (avg2s) as PLL input — prevents oscillation from instantaneous noise
- Adaptive gain:
Kp = 0.01 * (1 + 4 * min(|avg2s|/200, 1))— gentle when close (0.01), aggressive when far (0.05) - Log-compressed correction:
rate = baseRate + sign(avg2s) * Kp * log1p((|avg2s| - deadZone) / 100) - Dead zone: no correction when error < 5ms
- Rate learning: EMA (alpha=0.05) from 2-second observation windows, so corrections shrink as the true clock relationship is learned
- Hard seek fallback: if error exceeds 2 seconds, seeks directly (with 2-second cooldown)
After ~12-14 seconds (settle + calibration), the phone stays locked to FPP's position with 5-25ms steady-state error. The debug display shows live PLL state, error history, and playback rate.
- WebSocket transport: Python asyncio server polls FPP every 200ms, broadcasts
{state, base, pos_ms, mp3_url, server_ms}to all clients concurrently viaasyncio.gather()(prevents slow wlan1 clients from delaying wlan0 admin messages) - HTTP fallback: 250ms polling of
status.phpwhen WebSocket is unavailable - Clock offset: NTP-style estimation via WebSocket ping/pong, median filter + EWMA (alpha=0.3). Used in
serverOkcheck so sync works even when Pi clock is wrong - Clock set: Admin page detects >60s clock mismatch and can push phone time to Pi via
set_clockWebSocket message. Server usesdate -s @<unix_sec>withCAP_SYS_TIMEcapability (no sudo needed) - Elapsed handling: Messages older than 2 seconds (real elapsed) are discarded. No upper clamp — true elapsed is used for accurate target position
- PLL calibration: least-squares linear regression, 800ms minimum window, 6+ samples, base rate clamped to +/-1%
- Locked correction:
rate = baseRate + sign(avg2s) * Kp * log1p((|avg2s| - 5) / 100), Kp adaptive 0.01-0.05 - Error averaging: 2-second rolling window (avg2s) as PLL input, all-time average for diagnostics
- Hard seek: >2 seconds error, 2-second cooldown between seeks
- Rate learning: EMA alpha=0.05 from 2-second windows
- Position data:
milliseconds_elapsedfrom FPP API (notseconds_played, which is whole-seconds only) - Server timestamp:
server_mscaptured at midpoint of API call, used for clock offset calculation;round()notintval()to avoid 32-bit overflow on Pi 3B - Apache proxy:
mod_proxy_wstunnelproxies/wson port 80 to Python server on port 8080 - systemd service: runs as
fppuser with 64MB RAM / 25% CPU limits,CAP_SYS_TIMEfor clock set,NoNewPrivileges=truefor security - WiFi power save: Disabled on AP interfaces (
iw dev set power_save off) — brcmfmac enables power save at boot which causes E1.31 devices to drop - Playback control:
POST /api/commandwith "Start Playlist" / "Stop Now" commands - Audio unlock: browser autoplay policy requires a user gesture — first click/touch on the page silently plays and pauses to unlock the audio context
- Network isolation: nftables rules isolate show AP (wlan1) from admin AP (wlan0) — audience phones cannot reach admin pages, FPP settings, or E1.31 devices
- Policy routing: conntrack-based marks (0x64-0x6F) route replies back to the correct AP interface — supports up to 12 wireless interfaces
- BT delay compensation: optional per-profile delay (0-500ms) subtracted from PLL target position. Profiles stored in browser localStorage, calibrated via
calibrate.html - BT device management:
admin.phpprovides scan, pair, connect, disconnect, and volume control viabluetoothctlandpactl
For Bluetooth speakers connected to the Pi, audio arrives with a delay (typically 50-300ms). The calibration page helps measure and compensate for this: it plays synchronized click + flash patterns so you can dial in the exact offset for your speaker, then saves it as a named profile. The PLL subtracts this delay so lights and audio stay in sync.
The next step is native USB Bluetooth audio output from the Pi itself, allowing FPP to send show audio directly to a BT speaker without a phone relay. This would be a standalone FPP enhancement that works alongside SBS+:
- BT pairing UI — scan, pair, trust, and connect Bluetooth speakers from a web page
- ALSA bridge — use BlueALSA to present the paired BT speaker as a standard ALSA sound card that FPP can select as its audio output device
- Playback delay compensation — a configurable BT audio delay setting so FPP can start the FSEQ sequence ahead of the audio, keeping lights and sound in sync despite A2DP encoding latency (typically 100-300ms)
- Auto-reconnect — handle BT connection drops gracefully and reconnect without stopping the show
This feature is independent of the SBS+ plugin and could be contributed upstream to the FPP project.