Fix hyperhdrAdjust: use "brightness", not "scaleOutput"

Tested against a real HyperHDR instance rather than trusting the
schema further. scaleOutput (from the current schema-adjustment.json)
produced no visible change and no trace in serverinfo's echoed-back
adjustment state. brightness (0-100, absent from that same schema)
round-tripped correctly through serverinfo and visibly dimmed real
LEDs -- confirmed live, with piccap as the sole colour source and
only this sink's JSON-RPC calls changing anything.

A schema documents what a command accepts; it does not guarantee
what a given build actually does with each field, and this is a
mismatch between HyperHDR's current dev-branch schema and whatever
build the target instance is actually running. Switched the sink to
brightness (int 0-100) throughout: wire format, config defaults
(minBrightness/maxBrightness, 20-100), status fields, and the reset
sent on close. Renamed the frontend fields and mock to match.
Cross-compiles clean. Bumped to 1.0.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Rene Kievits
2026-08-26 14:59:51 +02:00
co-authored by Claude Opus 5
parent aae5a33283
commit e9f6c87d27
9 changed files with 80 additions and 53 deletions
+13 -8
View File
@@ -95,18 +95,23 @@ payload so nothing fragments on a normal Ethernet MTU.
### `hyperhdrAdjust` — brightness only, via HyperHDR's JSON-RPC ### `hyperhdrAdjust` — brightness only, via HyperHDR's JSON-RPC
Sends no image at all. Instead it calls HyperHDR's `adjustment` command — Sends no image at all. Instead it calls HyperHDR's `adjustment` command with
a post-processing stage that scales output brightness regardless of which a `brightness` value (0-100) — a post-processing stage that applies
priority is currently active — so an existing grabber or effect keeps regardless of which priority is currently active — so an existing grabber or
deciding colour and only overall brightness reacts to sound. capture app keeps deciding colour and only overall brightness reacts to
sound. Confirmed against a real HyperHDR instance by watching `brightness`
round-trip through `serverinfo` and the LEDs visibly respond; HyperHDR's own
`schema-adjustment.json` also documents a `scaleOutput` float (0.0-2.0), but
that field produced no effect on the same instance — what a schema declares
and what a given build actually acts on are not always the same thing.
| Key | Default | Meaning | | Key | Default | Meaning |
| --- | --- | --- | | --- | --- | --- |
| `host` | `""` | HyperHDR's address | | `host` | `""` | HyperHDR's address |
| `port` | `19444` | HyperHDR's classic JSON-RPC port (not 8090, not 19400) | | `port` | `19444` | HyperHDR's classic JSON-RPC port (not 8090, not 19400) |
| `level` | `"rms"` | `rms` (steadier) or `peak` (punchier) | | `level` | `"rms"` | `rms` (steadier) or `peak` (punchier) |
| `minScale` | `0.3` | brightness during silence; `1.0` is HyperHDR's normal level | | `minBrightness` | `20` | brightness (0-100) during silence |
| `maxScale` | `1.3` | brightness at full level; HyperHDR accepts up to `2.0` | | `maxBrightness` | `100` | brightness (0-100) at full level; HyperHDR does not go above 100 |
### `udp`, `tcp`, `http` ### `udp`, `tcp`, `http`
@@ -193,8 +198,8 @@ broken output does not take the others down.
Each sink adds its own fields. `packetsSent`/`bytesSent`/`sendErrors` for the Each sink adds its own fields. `packetsSent`/`bytesSent`/`sendErrors` for the
datagram sinks, `clients`/`droppedBytes` for the stream servers, datagram sinks, `clients`/`droppedBytes` for the stream servers,
`connected`/`registered`/`framesSent`/`connectFailures`/`lastError` for the `connected`/`registered`/`framesSent`/`connectFailures`/`lastError` for the
visualiser, `connected`/`updatesSent`/`minScale`/`maxScale`/`lastError` for visualiser, `connected`/`updatesSent`/`minBrightness`/`maxBrightness`/`lastError`
the brightness sink. for the brightness sink.
### Diagnostics ### Diagnostics
+22 -12
View File
@@ -169,15 +169,16 @@ lights go fully dark between beats, which looks dramatic and slightly broken.
## 4. Keep your grabber's colour, only pulse the brightness ## 4. Keep your grabber's colour, only pulse the brightness
For an ambilight-style setup that already has a real colour source — a For an ambilight-style setup that already has a real colour source — a
screen grabber, a USB capture card, an HDMI splitter — routes 1–3 all have screen grabber, a USB capture card, a webOS capture app like piccap — routes
the same problem: they compete for HyperHDR's priority and *replace* that 1–3 all have the same problem: they compete for HyperHDR's priority and
colour with something audio-derived. This route doesn't touch colour at all. *replace* that colour with something audio-derived. This route doesn't touch
colour at all.
HyperHDR has a JSON-RPC `adjustment` command that scales output brightness as HyperHDR has a JSON-RPC `adjustment` command that sets output brightness
a post-processing step, applied on top of whatever priority is currently (0-100) as a post-processing step, applied on top of whatever priority is
active. This sink sends nothing but that: no image, no priority currently active. This sink sends nothing but that: no image, no priority
registration, so the grabber keeps deciding hue and this only turns the registration, so the grabber (or piccap, or whatever else) keeps deciding
result up and down with the sound. hue and this only turns the result up and down with the sound.
``` ```
TV ──RTP or local──► audiocap-service ──JSON-RPC "adjustment"──► HyperHDR TV ──RTP or local──► audiocap-service ──JSON-RPC "adjustment"──► HyperHDR
@@ -192,12 +193,21 @@ TV ──RTP or local──► audiocap-service ──JSON-RPC "adjustment"─
| HyperHDR address | the HyperHDR machine's IP | | HyperHDR address | the HyperHDR machine's IP |
| JSON-RPC port | 19444 (HyperHDR's classic control port — not 8090, the web UI; not 19400, Flatbuffers) | | JSON-RPC port | 19444 (HyperHDR's classic control port — not 8090, the web UI; not 19400, Flatbuffers) |
| Follows | Average level (steadier) or Peak level (punchier) | | Follows | Average level (steadier) or Peak level (punchier) |
| Minimum brightness | `1.0` = HyperHDR's normal brightness; lower dims during quiet parts | | Minimum brightness | `0`-`100`; applied during quiet parts |
| Maximum brightness | up to `2.0`; boosts past normal on loud peaks | | Maximum brightness | `0`-`100`; applied at full level. HyperHDR does not go above 100 |
Needs a working capture source the same as every other route — see the top Needs a working capture source the same as every other route — see the top
of this document for picking one. On close, the sink resets `scaleOutput` to of this document for picking one. On close, the sink resets brightness to
`1.0` rather than leaving the LEDs stuck at whatever it last sent. `100` rather than leaving the LEDs stuck at whatever it last sent.
The field is `brightness`, confirmed by watching it round-trip through
`serverinfo` and the LEDs visibly respond on a real HyperHDR/Docker
instance. HyperHDR's current `schema-adjustment.json` documents a
`scaleOutput` float (0.0-2.0) instead, which looked like the obvious choice
and is what this sink sent originally — it had no visible or server-reported
effect on that same instance. If a future HyperHDR version drops
`brightness`, this needs re-verifying the same way, not just re-reading the
schema.
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "org.webosbrew.audiocap", "id": "org.webosbrew.audiocap",
"version": "1.0.3", "version": "1.0.4",
"vendor": "Homebrew", "vendor": "Homebrew",
"type": "web", "type": "web",
"main": "index.html", "main": "index.html",
+4 -4
View File
@@ -401,12 +401,12 @@
], ],
}, },
{ {
path: 'hyperhdrAdjust.minScale', label: 'Minimum brightness', type: 'number', path: 'hyperhdrAdjust.minBrightness', label: 'Minimum brightness', type: 'number',
hint: '1.0 is HyperHDR\'s normal brightness. Below that dims during quiet parts.', hint: '0-100. Applied during quiet parts. 100 is HyperHDR\'s normal brightness.',
}, },
{ {
path: 'hyperhdrAdjust.maxScale', label: 'Maximum brightness', type: 'number', path: 'hyperhdrAdjust.maxBrightness', label: 'Maximum brightness', type: 'number',
hint: 'Above 1.0 boosts past normal on loud peaks. HyperHDR accepts up to 2.0.', hint: '0-100. Applied at full level. HyperHDR does not go above 100.',
}, },
], ],
+1 -1
View File
@@ -16,7 +16,7 @@
mode: 'spectrum', saturation: 1.0, minBrightness: 0.02, mode: 'spectrum', saturation: 1.0, minBrightness: 0.02,
}, },
hyperhdrAdjust: { hyperhdrAdjust: {
host: '', port: 19444, minScale: 0.3, maxScale: 1.3, level: 'rms', host: '', port: 19444, minBrightness: 20, maxBrightness: 100, level: 'rms',
}, },
udp: { host: '', port: 4010, multicastTtl: 4 }, udp: { host: '', port: 4010, multicastTtl: 4 },
tcp: { port: 4011, maxClients: 4 }, tcp: { port: 4011, maxClients: 4 },
+2 -2
View File
@@ -51,8 +51,8 @@ static const char* DEFAULTS_JSON =
" \"hyperhdrAdjust\": {" " \"hyperhdrAdjust\": {"
" \"host\": \"\"," " \"host\": \"\","
" \"port\": 19444," " \"port\": 19444,"
" \"minScale\": 0.3," " \"minBrightness\": 20,"
" \"maxScale\": 1.3," " \"maxBrightness\": 100,"
" \"level\": \"rms\"" " \"level\": \"rms\""
" }," " },"
" \"udp\": { \"host\": \"\", \"port\": 4010, \"multicastTtl\": 4 }," " \"udp\": { \"host\": \"\", \"port\": 4010, \"multicastTtl\": 4 },"
+35 -23
View File
@@ -1,12 +1,22 @@
// Global brightness modulation via HyperHDR's own JSON-RPC "adjustment" // Global brightness modulation via HyperHDR's own JSON-RPC "adjustment"
// command, so a real picture source (HyperHDR's screen grabber, USB capture // command, so a real picture source (HyperHDR's screen grabber, a webOS
// card, whatever is already driving the LEDs) keeps deciding colour, and // capture app like piccap, whatever is already driving the LEDs) keeps
// only the overall brightness reacts to sound. Unlike every other sink here, // deciding colour, and only the overall brightness reacts to sound. Unlike
// this one deliberately sends no picture at all: send one would mean // every other sink here, this one deliberately sends no picture at all:
// competing for priority against the grabber, replacing its colour outright // sending one would mean competing for priority against that source,
// instead of layering on top of it. "adjustment" is a post-processing stage // replacing its colour outright instead of layering on top of it.
// that applies regardless of which priority is currently active, which is // "adjustment" is a post-processing stage that applies regardless of which
// exactly the layering this needs. // priority is currently active, which is exactly the layering this needs.
//
// The field is "brightness", an integer 0-100 -- confirmed against a real
// HyperHDR instance (serverinfo echoes it back, and the LEDs visibly
// responded). The "scaleOutput" float (0-2.0) in HyperHDR's current
// schema-adjustment.json looked like the obvious candidate and is what an
// earlier version of this file sent, but it produced no visible or
// server-reported effect on that same instance -- API docs describe the
// schema; they do not guarantee what a given build actually does with it.
// If a future HyperHDR drops "brightness" in favour of "scaleOutput", this
// will need re-verifying the same way, not just re-reading the schema.
// //
// Plain newline-delimited JSON over TCP -- HyperHDR's classic control port, // Plain newline-delimited JSON over TCP -- HyperHDR's classic control port,
// default 19444 -- a world simpler than the Flatbuffers image protocol the // default 19444 -- a world simpler than the Flatbuffers image protocol the
@@ -19,6 +29,7 @@
#include <arpa/inet.h> #include <arpa/inet.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <math.h>
#include <netdb.h> #include <netdb.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <poll.h> #include <poll.h>
@@ -38,8 +49,8 @@ typedef struct {
char host[128]; char host[128];
int port; int port;
float min_scale; int min_brightness; // 0-100
float max_scale; int max_brightness; // 0-100
bool use_rms; // rms is steadier than peak, which reacts to single transients bool use_rms; // rms is steadier than peak, which reacts to single transients
int fd; // -1 when not connected or still connecting int fd; // -1 when not connected or still connecting
@@ -154,11 +165,12 @@ static void adjust_write(sink_t* s, const int16_t* pcm, int frames, const dsp_le
level = 0; level = 0;
if (level > 1) if (level > 1)
level = 1; level = 1;
float scale = p->min_scale + level * (p->max_scale - p->min_scale); int brightness = p->min_brightness
+ (int)lroundf(level * (float)(p->max_brightness - p->min_brightness));
char msg[128]; char msg[128];
int n = snprintf(msg, sizeof(msg), int n = snprintf(msg, sizeof(msg),
"{\"command\":\"adjustment\",\"adjustment\":{\"scaleOutput\":%.3f}}\n", (double)scale); "{\"command\":\"adjustment\",\"adjustment\":{\"brightness\":%d}}\n", brightness);
if (n <= 0 || (size_t)n >= sizeof(msg)) if (n <= 0 || (size_t)n >= sizeof(msg))
return; return;
@@ -183,8 +195,8 @@ static void adjust_status(sink_t* s, json_writer_t* w)
jw_str(w, "target", p->host); jw_str(w, "target", p->host);
jw_int(w, "port", p->port); jw_int(w, "port", p->port);
jw_bool(w, "connected", p->connected); jw_bool(w, "connected", p->connected);
jw_num(w, "minScale", p->min_scale); jw_int(w, "minBrightness", p->min_brightness);
jw_num(w, "maxScale", p->max_scale); jw_int(w, "maxBrightness", p->max_brightness);
jw_int(w, "updatesSent", (long long)p->updates_sent); jw_int(w, "updatesSent", (long long)p->updates_sent);
jw_int(w, "connectFailures", (long long)p->connect_failures); jw_int(w, "connectFailures", (long long)p->connect_failures);
if (p->last_error[0]) if (p->last_error[0])
@@ -200,7 +212,7 @@ static void adjust_close(sink_t* s)
// Best effort: hand brightness back to normal rather than leaving // Best effort: hand brightness back to normal rather than leaving
// the LEDs stuck at whatever scale was last sent. // the LEDs stuck at whatever scale was last sent.
if (p->connected) { if (p->connected) {
static const char reset[] = "{\"command\":\"adjustment\",\"adjustment\":{\"scaleOutput\":1.0}}\n"; static const char reset[] = "{\"command\":\"adjustment\",\"adjustment\":{\"brightness\":100}}\n";
send(p->fd, reset, sizeof(reset) - 1, MSG_NOSIGNAL); send(p->fd, reset, sizeof(reset) - 1, MSG_NOSIGNAL);
} }
adjust_disconnect(p); adjust_disconnect(p);
@@ -214,8 +226,8 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
const json_value_t* sc = json_get(cfg, "hyperhdrAdjust"); const json_value_t* sc = json_get(cfg, "hyperhdrAdjust");
const char* host = json_str(sc, "host", NULL); const char* host = json_str(sc, "host", NULL);
int port = json_int(sc, "port", 19444); int port = json_int(sc, "port", 19444);
float min_scale = (float)json_num(sc, "minScale", 0.3); int min_brightness = json_int(sc, "minBrightness", 20);
float max_scale = (float)json_num(sc, "maxScale", 1.3); int max_brightness = json_int(sc, "maxBrightness", 100);
const char* level_source = json_str(sc, "level", "rms"); const char* level_source = json_str(sc, "level", "rms");
if (!host || !*host) { if (!host || !*host) {
@@ -226,8 +238,8 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
snprintf(err, errlen, "invalid HyperHDR JSON port %d", port); snprintf(err, errlen, "invalid HyperHDR JSON port %d", port);
return NULL; return NULL;
} }
if (max_scale <= min_scale) { if (min_brightness < 0 || max_brightness > 100 || max_brightness <= min_brightness) {
snprintf(err, errlen, "maxScale must be greater than minScale"); snprintf(err, errlen, "brightness range must be 0-100 with max greater than min");
return NULL; return NULL;
} }
@@ -261,8 +273,8 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
snprintf(p->host, sizeof(p->host), "%s", host); snprintf(p->host, sizeof(p->host), "%s", host);
p->port = port; p->port = port;
p->min_scale = min_scale; p->min_brightness = min_brightness;
p->max_scale = max_scale; p->max_brightness = max_brightness;
p->use_rms = strcmp(level_source, "peak") != 0; p->use_rms = strcmp(level_source, "peak") != 0;
s->driver = &sink_driver_hyperhdr_adjust; s->driver = &sink_driver_hyperhdr_adjust;
@@ -272,8 +284,8 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
s->status = adjust_status; s->status = adjust_status;
s->close = adjust_close; s->close = adjust_close;
INFO("HyperHDR adjustment sink: %s:%d, scaleOutput %.2f..%.2f from %s", host, port, INFO("HyperHDR adjustment sink: %s:%d, brightness %d..%d from %s", host, port,
min_scale, max_scale, p->use_rms ? "rms" : "peak"); min_brightness, max_brightness, p->use_rms ? "rms" : "peak");
return s; return s;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "lgtv-audio-cap", "name": "lgtv-audio-cap",
"version": "1.0.3", "version": "1.0.4",
"private": true, "private": true,
"description": "Captures audio on an LG webOS 5/6 TV and streams it out \u2014 HyperHDR first, plus raw UDP, TCP and HTTP.", "description": "Captures audio on an LG webOS 5/6 TV and streams it out \u2014 HyperHDR first, plus raw UDP, TCP and HTTP.",
"keywords": [ "keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"id": "org.webosbrew.audiocap.service", "id": "org.webosbrew.audiocap.service",
"version": "1.0.3", "version": "1.0.4",
"description": "Captures TV audio and streams it to HyperHDR and other receivers", "description": "Captures TV audio and streams it to HyperHDR and other receivers",
"main": "audiocap-service" "main": "audiocap-service"
} }