How I Replaced the $300 Philips Hue Sync Box with Python and a Raspberry Pi
Real-time spectral analysis, beat detection via spectral flux, and four sync modes — all running on a Pi 5 with NumPy and a named pipe.
By Igor Riera
The Philips Hue Sync Box costs $300 and requires Hue’s cloud infrastructure to function. It takes HDMI input from a single source, analyzes the video signal, and maps colors to your Hue lights. It’s a neat product. I built a software replacement that costs nothing in additional hardware, works with any audio source, and runs entirely on my local network.
Here’s how the audio analysis pipeline actually works.
The audio path
Music enters my system through three sources: Spotify Connect (via Music Assistant), AirPlay (shairport-sync), and Bluetooth. All three route into Snapcast, which handles synchronized multi-room audio distribution across three zones.
The analysis pipeline taps into the Snapcast stream. A small Python script called snap_tap.py acts as a Snapcast client — it connects to the server on port 1704, receives raw PCM audio chunks via the Snapcast wire protocol, and writes them to a named pipe at /tmp/snapfifo_analysis. Non-blocking writes mean if the analyzer falls behind, chunks are dropped rather than backed up. The audio stream is never interrupted.
The analyzer reads from that pipe. The input format is 48 kHz, 16-bit signed little-endian stereo — 512 samples per chunk, which means roughly 47 analysis frames per second.
Spectral analysis
Each 512-sample chunk gets zero-padded to 1024 samples and windowed with a Hanning function. A real FFT via NumPy produces 513 frequency bins. These get split into three bands:
- Bass: bins 0–19 (roughly 0–900 Hz)
- Mid: bins 20–79 (~900 Hz to 3.7 kHz)
- Treble: bins 80+ (3.7 kHz and above)
The raw energy in each band is weighted with pink noise compensation — bass at 1.0x, mids at 4.0x, treble at 12.0x. Without this, bass-heavy music dominates everything. The human ear is more sensitive to midrange and treble, and the compensation curve reflects that. These weights were tuned empirically, not calculated — I tested with a range of genres until the color distribution felt balanced.
Beat detection
Beat detection uses a SuperFlux-inspired spectral flux algorithm. The idea: compare each spectrum to the previous one, but max-filter the previous spectrum across neighboring bins first. This suppresses vibrato and tremolo artifacts that would otherwise trigger false beats.
The flux value — the sum of positive differences between current and max-filtered previous spectrum — represents “novelty” in the audio. A beat is detected when the flux exceeds an adaptive threshold: the mean of the last 100 flux values plus 1.5 standard deviations.
A minimum beat interval of 0.2 seconds caps detection at 300 BPM. BPM is estimated from the median of the last 20 inter-beat intervals, which stabilizes after about 10–15 seconds of consistent audio.
None of this is novel. The spectral flux approach is well-documented in music information retrieval literature. LedFx and WLED both use similar techniques. I adapted the implementation to work with the Snapcast pipe format and Home Assistant’s light control API.
Smoothing: the part that makes it feel right
Raw spectral data looks terrible on lights. It flickers, it jumps, it has no musical feel. The smoothing layer is what turns data into something that looks intentional.
I use an asymmetric exponential filter — fast rise (alpha 0.95–0.99), slow decay (alpha 0.1–0.2). Beats punch through immediately. Fades are smooth and musical. This is the same technique LedFx uses, and it’s the single biggest contributor to making the lights feel like they’re responding to music rather than just reacting to noise.
Automatic gain control normalizes the output regardless of source volume. It tracks the recent peak level with a fast attack and slow release, then divides the current value by the tracked peak. Quiet jazz and loud electronic music both produce a full 0–1 output range. Without AGC, you’d be constantly adjusting a sensitivity knob.
Mapping to lights
Brightness follows a perceptual power curve — energy raised to 0.6, then mapped to a configurable range. The curve compensates for the fact that LEDs have a non-linear relationship between duty cycle and perceived brightness.
Color maps spectrum to RGB directly. Bass energy drives red, mids drive green, treble drives blue, with cross-blending terms so the colors don’t look harsh. The output is normalized to full saturation — there’s always a dominant color, never a washed-out white.
The analyzer sends HTTP requests to Home Assistant’s light.turn_on service at 15 Hz — one update every ~67 milliseconds. Each request specifies brightness, RGB color, and a transition time that varies by mode.
Four modes
Pulse is the party mode. Low ambient brightness (5–80 range), but on every detected beat, all lights flash to 255 with zero transition time. Between beats, the spectrum colors play at low intensity. High contrast, BPM-aware.
Cycle is the dinner party mode. Brightness tracks energy in a moderate range (40–200). Colors shift with the spectrum but transitions are slow — 500 milliseconds. No beat flashing. Smooth and unobtrusive.
Reactive is the full experience. Brightness spans the entire range (10–255). Colors track spectrum. Transition speed is adaptive — fast on peaks (0 ms), slow in quiet passages (200 ms). This is the default mode and the one I use most.
Cinema is for movies. Dim brightness (10–80), warm fixed palette (amber tones), 1-second transitions. The lights glow softly in response to the soundtrack without ever being distracting.
All four modes are switchable live from the iOS app via Home Assistant’s input_select helper. The analyzer polls the helper every 5 seconds — the mode change takes effect on the next analysis frame.
Running it
The whole thing runs as a systemd service on the Pi 5. It starts on boot, recovers automatically on crash (5-second restart delay), and logs via journalctl. CPU usage sits around 5–10% of one core — NumPy’s FFT is well-optimized on ARM.
Dependencies are minimal: NumPy, SciPy, httpx for async HTTP, PyYAML for config. No frameworks, no message brokers, no databases. A Python script reading from a pipe and making HTTP calls.
The Hue Sync Box would have cost $300, required HDMI passthrough, locked me to video input only, and depended on Philips’ cloud service. This handles any audio source, runs locally, and cost nothing beyond the Pi and Hue lights I already owned.
Sometimes the right solution is 479 lines of Python and a named pipe.