A Raspberry Pi AirPlay receiver that lights up your Nanoleaf panels in time with the music. Stream from any iPhone or Mac, and your Nanoleaf panels become a reactive music visualizer.
The Pi runs a single container that bundles shairport-sync (AirPlay 2 via
nqptp), the audio visualizer, and a web control panel. Pair your Nanoleaf
device from the browser, then play to the AirPlay receiver — that's it.
NanoViz started as a fork of audioleaf and has evolved into a complete Raspberry Pi AirPlay receiver + Nanoleaf visualizer appliance. See CHANGELOG.md for history.
-
AirPlay 2 receiver — Stream from any Apple device; appears as a regular AirPlay target on your network.
-
Volume slider drives brightness — The iOS/macOS volume slider dims and brightens your panels. Audio stays at full volume; only the lights move.
-
Three visualizer effects — Spectrum (bass→treble across panels), Energy Wave (cascading ripples), and Ripple (transient-driven pulses).
-
Album art palette — Optionally pull palette colors from the current track's cover art instead of a fixed palette.
-
Device palettes — Use any palette saved as a Nanoleaf "effect" on the device itself; no static catalog to maintain.
-
Web UI — Pair devices, switch effects/palettes, see now-playing, live panel preview, and a layout visualizer.
-
Multi-instance — Run more than one receiver per Pi with
--name=....
One-shot install from a fresh Pi:
curl -fsSL https://raw.githubusercontent.com/Weekendsuperhero-io/nanoviz/main/pi/setup.sh \
| sudo bashOr from a local clone:
sudo ./pi/setup.shThe installer:
- Installs
podmanand thesnd-aloopkernel module. - Adds your user to
audio,render, andsystemd-journalgroups. - Stages
~/<USER>/.config/nanoviz/(config + compose file). - Drops a Quadlet at
/etc/containers/systemd/nanoviz.containerand startsnanoviz.service. - Installs a polkit rule so the
audiogroup cansystemctl start/stop/restartwithout sudo.
After install, do these steps in order:
-
Open the web UI at
http://<pi-ip>:8787 -
Set the audio backend (most important step):
-
Go to Visualizer settings → Audio Backend
-
Paste exactly this (including the commas and numbers):
hw:CARD=Loopback,DEV=1 -
Save the setting. This tells NanoViz to listen to the audio coming from AirPlay on your Pi.
-
-
Pair your Nanoleaf device from the Devices → Pair new device card.
-
AirPlay from your iPhone or Mac to the receiver (it will appear as nanoviz or whatever you set with
--airplay-name).
Log out and back in once if the install script added you to new groups.
All output from NanoViz, shairport-sync, and nqptp goes to the system journal.
# Follow live logs (recommended)
journalctl -fu nanoviz
# For a named instance (example)
journalctl -fu nanoviz-bedroomBecause the installer added you to the systemd-journal group, you can run this without sudo.
Pro tip: If you're debugging AirPlay or audio issues, run the above and then trigger the behavior you're investigating (connect AirPlay, play music, pause, etc.).
How to find the correct audio backend string on your Pi (only if the above doesn't work):
aplay -l | grep -i loopbackUse the hw:CARD=...,DEV=... value for the Loopback card that shairport-sync is sending audio to (usually DEV=1).
--name=NAME Instance name (default "nanoviz"). Controls the
systemd unit, container name, Quadlet filename,
polkit rule, and config dir. Use a distinct name to
run multiple receivers on one host.
--airplay-name=NAME AirPlay display name advertised on mDNS (the name
that appears in the iOS/macOS AirPlay picker).
Spaces are allowed; quote the value.
--image-tag=TAG Container image tag (default: "dev" on non-main git
branches, "latest" otherwise).
--config-dir=DIR Override the default config dir
(~/.config/<name> for sudo'd users,
/etc/<name> for raw-root installs).
--no-systemd Skip the systemd unit; just `podman compose up -d`.
--no-deploy Host prep only — don't pull or start the container.
--force-compose Overwrite the staged compose.yaml if it exists.
Example — second receiver named "Bedroom":
sudo ./pi/setup.sh --name=nanoviz-bedroom --airplay-name="Bedroom Speaker"sudo podman pull ghcr.io/weekendsuperhero-io/nanoviz:latest
sudo systemctl restart nanoviz(or :dev if you installed from a non-main branch).
sudo ./pi/uninstall.sh # keep config
sudo ./pi/uninstall.sh --purge --remove-image # full wipe
sudo ./pi/uninstall.sh --name=nanoviz-bedroom # named instanceRemoves the systemd service, Quadlet, container, and polkit rule.
~/.config/<name>/ (with your nl_devices.toml pairing tokens) is kept
unless you pass --purge.
-
Open the AirPlay menu on your iPhone / Mac and pick nanoviz (or whatever you set via
--airplay-name). -
Start playback. Panels react.
-
Slide the volume bar on iOS/macOS — panel brightness tracks the slider. Audio playback volume is unaffected. Full details under Brightness.
-
If you need to debug anything, see the Viewing logs section.
-
Open
http://<pi-ip>:8787to change effect, palette, sort axis, or audio sensitivity (default_gain) at runtime.
- Spectrum — Each panel tracks a frequency band. Bass on one end, treble on the other.
- Energy Wave — Per-panel band like Spectrum, but brightness cascades across the layout as a traveling wave.
- Ripple — All panels pulse together, driven by audio transients.
Pick one in the web UI:
-
Palette — Use a named palette stored as an effect on your Nanoleaf device. The dropdown lists every palette the device knows about and shows the swatches inline.
-
Artwork — Extract colors from the current track's cover art. Falls back to the configured palette when nothing's playing.
Panel brightness is driven by the AirPlay volume slider on the streaming device (iPhone Control Center, macOS menu-bar volume, the volume slider in any AirPlay-aware app). Slide it up — panels get brighter; slide it down — panels dim.
- Audio is not affected. The shipped
shairport-sync.confsetsignore_volume_control = "yes", so AirPlay volume events update panel brightness only. Music stays at full volume the whole time. If you want lower playback volume, use your speaker / amp's own volume, not the AirPlay slider. - Mapping.
0 dB(slider at the top) → brightness100;-30 dB(slider at the bottom) → brightness1; linear in between. Tapping Mute holds the current brightness rather than darkening the panels (so a stray mute press doesn't kill the lights). - Manual override in the web UI. The brightness slider on each device card in the web UI still works — set any value you like. The next AirPlay volume event will take control again. This is intentional: the AirPlay slider is the "live" knob; the web slider is for one-off adjustments when nothing's playing.
- Connection behavior. On AirPlay connect, the panels keep whatever brightness they had before; brightness only changes when the slider actually moves. (Previously the panels jumped to 100 on every connect.)
default_gainis a separate knob. It controls audio analysis sensitivity (how loud the FFT input has to be before the visualizer swings full-range), not panel brightness. Set it once and forget; the volume slider is what you reach for daily.
If you'd rather wire a different control to brightness, the underlying
endpoint is PUT /api/devices/{name}/state with {"brightness": 0-100}.
Config lives in ~/.config/<instance>/config/config.toml on the host
(bind-mounted into the container as /root/.config/nanoviz/). The web
UI edits the running config in memory; click Save to persist.
default_nl_device_name = "Shapes AC01"
[visualizer_config]
audio_backend = "hw:CARD=Loopback,DEV=1" # Raspberry Pi with default snd-aloop setup (see "After install" section above)
freq_range = [20, 4500]
color_source = "palette" # "palette" or "artwork"
palette_name = "Sunset" # any palette name saved on the device
default_gain = 1.0
transition_time = 2 # in 100ms units
time_window = 0.1875
primary_axis = "Y" # "X" or "Y"
sort_primary = "Asc" # "Asc" or "Desc"
sort_secondary = "Asc"
effect = "Spectrum" # "Spectrum" | "EnergyWave" | "Ripple"nl_devices.toml (next to config.toml) holds your Nanoleaf pairing
tokens. The web UI writes this automatically when you pair from the
Devices card. Keep it backed up if you reinstall.
The container exposes a REST + WebSocket API on port 8787. Selected
routes:
GET /api/health— version + the configured AirPlay nameGET /api/config— current runtime configPOST /api/config/save— persist runtime config toconfig.tomlPUT /api/config/visualizer/{effect,palette,color-source,sort,settings}GET /api/now-playing— current track + AirPlay volumeGET /api/now-playing/artwork— current cover art bytesGET /api/visualizer/{preview,status}andWS /api/visualizer/wsGET /api/palettes— palettes from the active deviceGET /api/audio/backendsGET /api/devices,POST /api/devices/discover,POST /api/devices/pairGET /api/devices/{name}/info,/layoutPUT /api/devices/{name}/state— manual power / brightness override
The repo builds and runs as a regular Rust binary plus a Vite frontend. Useful on macOS or Linux dev machines where you don't want to deal with the container.
# 1. Build the frontend
cd web && pnpm install && pnpm build && cd ..
# 2. Run the API + visualizer
cargo run --bin nanovizThen visit http://127.0.0.1:8787.
Useful flags:
--host 0.0.0.0— bind interface (default0.0.0.0).--port 8787— HTTP port.--config /path/to/config.toml--devices /path/to/nl_devices.tomlNANOVIZ_FRONTEND_DIR=/path/to/web/dist— serve a different build.NANOVIZ_AIRPLAY_NAME="Living Room"— override AirPlay name (also surfaced in the web UI header).
For the Vite dev server with hot reload:
cd web && pnpm dev # http://127.0.0.1:5173 (proxies /api → 8787)The CoreAudio backend can't intercept system audio without a loopback device.
- Install BlackHole (free).
- Audio MIDI Setup → Multi-Output Device with your speakers + BlackHole 2ch; set as system output.
- In the web UI, switch the audio backend to
BlackHole 2ch.
In pavucontrol → Recording tab, point nanoviz at your player's
monitor source. Pick the matching backend in the web UI.
- AirPlay name keeps incrementing (
nanoviz #2,#3, ...) — A stale mDNS registration is cached on the host's avahi-daemon. Runsudo systemctl restart avahi-daemononce. See DEBUG.md. - Web UI says "No known devices yet" — Click Pair new device → Scan network, hold the Nanoleaf power button until the LEDs flash, then click Pair.
- Panels don't react — Check the visualizer status badge in the web
UI; if it's
Restarting, the audio backend may be missing.default_gaincontrols audio sensitivity; bump it in the web UI. - Container won't start with "Unit is transient or generated" —
Older installs; pull the latest
pi/setup.sh. The Quadlet generator wires[Install] WantedBy=itself; never callsystemctl enableon it.
Issues and PRs welcome. Run cargo clippy --bin nanoviz -- -D warnings
and pnpm build (in web/) before sending.
NanoViz is a fork of audioleaf by Antoni Zasada, which provided the original Nanoleaf SSDP discovery, ALSA audio capture, FFT analysis, panel sorting/layout, and palette visualizer effects. The rename to NanoViz coincided with the shift from a terminal-only macOS tool to a Pi-first AirPlay appliance with a web UI (AirPlay 2 via shairport-sync + nqptp, podman/Quadlet deployment, web-based device pairing, album-art palette extraction, AirPlay-volume brightness, and the React control panel).
See NOTICE for the fork lineage and third-party component attributions. MIT licensed; see LICENSE.



