Audio-Reactive Lighting
The bulb reacts to sound. Twenty ways.
Capture from a VoiceMeeter/virtual-cable device (whatever's playing on your PC) or a real microphone (whatever's audible in the room). Pick a mode, hit start.
If you run this in Docker, read this first. A Linux container on Docker Desktop for Windows has no access to the host's audio devices, so a session started there runs, reports itself running, and never reacts to a sound — no error, just a bulb that sits still. Two supported ways round it: bridge mode (a small Windows tool streams audio in; the dashboard keeps serving) or native mode (one command swaps the container for a host backend). See Bridge vs. native.
The 20 modes
| Mode | What it does |
|---|---|
band_fixed | Bass/mid/treble blended into hue by whichever is loudest — bass→red/orange, mid→green, treble→blue. |
dominant_band | Hue smoothly follows whichever band currently dominates. |
weighted_blend | Continuous hue from the spectral centroid, not fixed anchors. |
vu_meter | Fixed hue, brightness only — a direct volume meter. |
auto_rotate_hue | Hue cycles on its own; brightness/beats still track the audio. |
monochrome_pulse | One color, pulses brighter on the beat. |
strobe_on_drop | Dim warm baseline; a hard bass hit triggers a full white flash. |
palette_cycle | Steps through the 25 built-in presets, one step per beat. |
spectrum_gradient | Continuous hue gradient across a configurable 3–16 band split. |
band_flash_overlay | Ambient gradient base, with accent flashes when any band spikes. |
stereo_split | Hue leans toward a left/right anchor based on channel balance. |
breathing_silence | Slow ambient breathing during quiet passages instead of going flat. |
harmonic_pairs v0.3.0 | Finds the two most-energetic non-adjacent bands and blends between two complementary (180°-apart) hue anchors by their energy ratio. |
kick_snare_split v0.3.0 | Bass energy drives brightness (kick-like pulses); a mid/high band drives a hue swing on top (snare/hihat accent). |
energy_contour | Hue follows the overall energy envelope rather than any single band. |
bass_only_pulse | Ignores everything above the low band — pure kick response. |
mirror_mode | Hue reflects around a fixed centre point instead of travelling in one direction. |
random_walk_hue | Hue drifts by bounded random steps, nudged by the audio rather than driven by it. |
silence_flash_recover | Marks the return of sound after a long pause with a flash. |
crescendo_ramp | Tracks whether energy is building over a couple of seconds and ramps with it. |
Speech is not music: on a podcast or a stream, the bass-led modes barely move and beat detection is close to meaningless. Reach for the level-driven ones — vu_meter, energy_contour, weighted_blend — and a longer dwell.
A real flicker bug was found and fixed while building harmonic_pairs: two hue anchors exactly 180° apart cancel to a zero vector under a shortest-arc blend at a 50/50 energy split, so it walks a fixed direction between the anchors instead — caught by its own test suite before shipping.
Bridge vs. native
Both are first-class. Pick by what you're doing:
| Bridge mode | Native mode | |
|---|---|---|
| Dashboard | keeps serving from the container | container stopped, host serves |
| Added capture latency | ~1–2ms loopback hop | zero |
| Start it | tools\start-audio-bridge.cmd | tools\native-audio-mode.cmd |
| Use it for | everyday and remote use | judging presets by ear |
Native mode serves the same port, so the local and tailnet URLs both keep working across the switch, and the container is restored on every exit path — Ctrl-C, a crash, a hard kill. Bridge mode's capture device is chosen in the dashboard: the list is reported by the capture tool itself, because the container cannot enumerate the host's audio devices. Loopback devices are grouped above microphones, since a microphone hears the room rather than what the computer is playing.
Latency, measured rather than claimed
Analysis runs on a short hop over a longer window, because one number cannot do both jobs: the hop sets latency, the window sets frequency resolution. Shrinking a single combined block buys latency and destroys the low-frequency resolution beat detection depends on.
- Hop — 256 samples (5.8ms). How often a colour decision is made. Shorter costs CPU; the default runs at roughly 3% of one core.
- Window — 1024 samples (23.2ms, 43Hz per bin). How much audio each decision sees. Longer is not better: raising it to 2048 measurably costs tempo accuracy, because a longer window smears the transient it is trying to locate.
- Display dwell (
min_dwell_ms) — how long a colour stays before the next replaces it. Default 90ms, floor 40ms. Editable while a session runs, and saved when you move it.
The dashboard reports each stage live, with a typical figure and a worst case, so latency is a number you can read rather than a claim. Measured on real hardware:
| Stage | Typical | p95 | Who controls it |
|---|---|---|---|
| Capture | 5.8ms | 27.4ms | you (hop size) |
| Analysis | 0.6ms | 0.9ms | you (window size) |
| Software total | 6.4ms | 28.3ms | you |
| Bulb round-trip | 11.2ms | 63.6ms | nobody |
The bulb is the floor, and it is not close. Its round-trip was measured between 11ms and 637ms on the same lamp within an hour — at worst about a hundred times the entire software budget. No setting in this project changes that; it is Wi-Fi, Tuya's protocol and the lamp's firmware. If the lights feel laggy and the dashboard shows the software stages in single-digit milliseconds, the answer isn't a setting.
The sender always transmits the freshest computed value when its dwell window opens — never a backlog — so raising the analysis rate never causes lag.
Multi-bulb orchestration
One shared audio analysis drives every bulb in a group via one of five role modes:
| Role | Effect |
|---|---|
unison | Every bulb shows the identical color. |
phase_offset | Same effect, hue shifted per bulb — a chase. |
band_split | Bulb i primarily reacts to band i — a literal “bass bulb”/“treble bulb” setup. |
wave | Identical colour, with a brightness crest travelling across the ordered bulb list. |
mirror | Bulbs paired front-to-back; each pair's second bulb mirrors the first's hue. |
Each bulb keeps its own independent send thread, so one slow or offline bulb in a group never stalls the others.
Known limitation: the 24 genre presets are reasoned, not tuned by ear. Every blocker to doing it is now cleared — audio reaches the container, the capture device is selectable in the UI, dwell is adjustable live, and latency is measured and inside budget — but nobody has yet sat down with real music and used their judgement. See the Build History.
Only one physical bulb exists. Every multi-bulb feature above — groups, zones, the five role modes, failover — is tested against fakes and has never run on real hardware.
Full design rationale, the two real bugs found while building this (a blocking-I/O freeze and a circular hue-smoothing bug), and setup details: the complete design doc, or the step-by-step skill.
Every knob with its cost, a symptom-to-setting troubleshooting table, the full latency budget and Windows audio-source setup: the audio latency guide. All 20 modes explained with tuning guidance: the modes reference. Both are also browsable inside the dashboard under System → Docs.