Diagnosing capture on a real TV meant reading pactlSources off the screen and typing an exact PulseAudio source name back in through the same remote-driven text field — no way to copy-paste, easy to mistype, and the one piece of information (which source, if any, is actually RUNNING) was buried in a JSON dump. Added two choice() pickers bound to the same capture.device setting: one built from pactlSources (pulse/auto backends), one built from alsaCapturePcms (alsa backend), both parsed from diagnostics the service already collects — no new Luna method needed. Diagnostics already run once at boot, so the picker is populated immediately, before the user ever presses "Run diagnostics" by hand. Picking a value writes straight into capture.device, and the plain text field stays as the fallback for anything the parser misses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.8 KiB
Connecting to HyperHDR
HyperHDR's music effects read a local capture device. There is no network audio input to send to, no API to push samples into. Everything below is a way of working around that.
Three routes, in the order you should try them.
1. RTP audio into a loopback device (recommended)
The TV sends RTP/L16 to the HyperHDR machine; a receiver there plays it into a loopback; HyperHDR captures the other end of that loopback. HyperHDR sees a perfectly ordinary sound card and does its own analysis, so every effect works exactly as it would with a real input.
TV ──RTP/L16 udp/5004──► lgtv-audiocap-receiver.py ──► hw:Loopback,0,0
║ snd-aloop
HyperHDR ◄──── hw:Loopback,1,0
On the HyperHDR machine
git clone <this repo> && cd lgtv-audio-cap
sudo ./host/install-loopback.sh --install-service
That loads snd-aloop (persisting it across reboots), keeps PulseAudio's hands
off the loopback card, installs the receiver into /usr/local/bin and starts it
as a systemd unit. It finishes by printing the exact device name to give
HyperHDR.
To do it by hand instead:
sudo modprobe snd-aloop index=10 pcm_substreams=1 id=Loopback
./host/lgtv-audiocap-receiver.py --output aplay --device hw:Loopback,0,0
On Unraid
Unraid boots from a read-only USB image, so nothing here can be "just a systemd service" — the loopback and the receiver need to be split into the one part that genuinely needs the bare-metal kernel and the part that doesn't.
The loopback (bare metal): install
unraid/lgtv-audiocap-loopback.plg —
Plugins → Install Plugin, paste the raw URL to that file. It loads
snd-aloop immediately and adds one line to /boot/config/go so it survives
a reboot; Plugins → Uninstall removes exactly that line and nothing else.
The receiver (a normal container): build
docker/Dockerfile and add it like any other Unraid
container — Docker → Add Container:
| Setting | Value |
|---|---|
| Repository | your image, e.g. 192.168.0.4:5000/lgtv-audiocap-receiver |
| Network Type | Bridge (or Host, either works — it only ever listens on one UDP port) |
| Port | 5004 UDP → 5004 |
| Extra Parameters | --device /dev/snd:/dev/snd |
It's entirely configured through environment variables — see
docker/entrypoint.sh for the full list
(PORT, DEVICE, RATE, CHANNELS, LATENCY_MS, …). The default DEVICE
is already hw:Loopback,0,0, so nothing needs setting for the common case.
Point the HyperHDR container at the loopback the same way: add
--device /dev/snd:/dev/snd to its extra parameters too, then use
hw:Loopback,1,0 in its Sound Capture settings. Both containers reach the
same host kernel device, so no networking between them is needed for this
part — only the TV needs to know the host's IP, for the RTP stream itself.
On the TV
Outputs → HyperHDR audio (RTP/L16)
| Setting | Value |
|---|---|
| Receiver address | the HyperHDR machine's IP |
| UDP port | 5004 |
| Multicast | off |
| Announce over SAP | on (harmless, and needed for route 2) |
Press Start.
In HyperHDR
Settings → Sound capture (in newer builds; older ones put it under the music
effect itself) → input device hw:Loopback,1,0, then choose a music effect.
Checking it
# is anything arriving at all?
./host/lgtv-audiocap-receiver.py --port 5004 --output - | \
aplay -f S16_LE -r 48000 -c 2 -
The receiver prints a line every 30 seconds with packet, loss and restart counts. Losses in the low hundreds over hours are normal on Wi-Fi; a steady stream of them means the TV's Wi-Fi is the bottleneck and the set really wants Ethernet.
2. RTP straight into PulseAudio, nothing installed
If the HyperHDR machine runs PulseAudio or PipeWire and HyperHDR can reach it
through the ALSA pulse device, you do not need the receiver at all. The TV
announces the stream over SAP and PulseAudio builds a source from it.
pactl load-module module-rtp-recv sap_address=224.0.0.56
With Announce over SAP enabled on the TV, a source called something like
rtp_recv.LG TV Audio Cap appears within five seconds. Point HyperHDR at its
monitor.
This is the least code, but it is also the least predictable: PulseAudio's RTP receiver has no jitter buffer worth the name, and PipeWire's compatibility layer does not always implement the module. Treat it as a nice surprise if it works.
For unicast rather than SAP discovery, turn Announce over SAP off and load:
pactl load-module module-rtp-recv sap_address=0.0.0.0 port=5004
3. The TV does the visualising
No host software, no sound device. The TV analyses the audio, renders a small
image and sends it to HyperHDR's Flatbuffers port, the same way a
hyperion-remote or a screen grabber would.
Outputs → HyperHDR visualiser
| Setting | Value |
|---|---|
| HyperHDR address | the HyperHDR machine's IP |
| Flatbuffers port | 19400 |
| Style | Spectrum, Level bar or Pulse |
| Priority | 150 (lower numbers win in HyperHDR) |
In HyperHDR, make sure the Flatbuffers server is enabled (Settings → Network Services → Flatbuffers server, default port 19400).
What you give up: HyperHDR's own effects, colour calibration on the audio path, and any hope of the lights matching an effect you have configured elsewhere. The TV decides what the lights show. What you gain: it works in about a minute.
The three styles:
- Spectrum — 16 bands across the image, hue by frequency.
- Level bar — one bar that tracks the overall level.
- Pulse — the whole image flashes with the beat.
saturation and minBrightness shape the output; minBrightness: 0 lets the
lights go fully dark between beats, which looks dramatic and slightly broken.
Which one to use
| Route 1 | Route 2 | Route 3 | |
|---|---|---|---|
| Host software | receiver + loopback | none | none |
| HyperHDR effects | all of them | all of them | none, the TV renders |
| Latency | ~100 ms | ~100 ms, less stable | ~40 ms |
| Robustness | good | depends on your PulseAudio | good |
| Setup time | 10 minutes | 2 minutes if it works | 1 minute |
Route 1 unless you have a reason.
Latency
Roughly, end to end on route 1:
| Stage | Typical |
|---|---|
| TV capture block | 11 ms (512 frames at 48 kHz) |
| Network | 1–5 ms wired, 5–40 ms Wi-Fi |
| Receiver prebuffer | 60 ms, --prebuffer-ms |
| Playback buffer | 80 ms, --latency-ms |
| HyperHDR's own analysis | 20–50 ms |
Around 150–200 ms in total, which for ambient lighting is imperceptible. If you
want it tighter, lower --latency-ms and --prebuffer-ms until the audio
starts crackling, then go back up one step.