Files
lgtv_audio_cap/test/engine_smoke.c
T
Rene KievitsandClaude Opus 5 7529a60650 Audio capture and streaming app for webOS 5/6
Captures the TV's audio and sends it out over several transports. The
primary one is HyperHDR: RTP/L16 to a host-side loopback device, since
HyperHDR has no network audio input of its own. A second route renders
the spectrum on the TV and sends FlatBuffers images to port 19400
instead, for setups where touching the host's sound config is not an
option.

  native/       the service: capture backends (PulseAudio, ALSA, exec,
                test tone, all dlopen-based), DSP, and one file per sink
  frontend/     D-pad driven UI at a fixed 1920x1080
  servicefiles/ native service manifest plus the boot script
  host/         RTP receiver and the loopback installer for the HyperHDR
                machine
  tools/        build/package, asset generation, Homebrew Channel
                manifest, on-TV probe
  test/         host-side suites: FlatBuffers and RTP verified against
                real decoders, the engine end to end, the page in jsdom

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 10:21:00 +02:00

275 lines
7.8 KiB
C

// End-to-end check of the capture pipeline, minus webOS.
//
// Runs the engine with the `tone` backend feeding the TCP and HTTP sinks, then
// connects to both as a client and verifies that real audio comes out with the
// right framing. Everything here works identically on the TV; only the Luna
// layer is missing, so this exercises capture -> DSP -> fan-out -> socket.
//
// Build and run: test/run-tests.sh
#include "common/log.h"
#include "engine.h"
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define HTTP_PORT 45812
#define TCP_PORT 45811
static int failures = 0;
static void check(bool ok, const char* what)
{
printf("%s %s\n", ok ? " ok " : " FAIL", what);
if (!ok)
failures++;
}
static void sleep_ms(int ms)
{
struct timespec ts = { .tv_sec = ms / 1000, .tv_nsec = (long)(ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
static int connect_local(int port)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
return -1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
close(fd);
return -1;
}
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
return fd;
}
// Reads exactly `len` bytes or fails.
static bool read_exact(int fd, void* dst, size_t len)
{
size_t got = 0;
while (got < len) {
ssize_t n = read(fd, (char*)dst + got, len - got);
if (n <= 0)
return false;
got += (size_t)n;
}
return true;
}
static uint32_t rd_u32le(const uint8_t* p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static uint16_t rd_u16le(const uint8_t* p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
// True if the buffer contains something other than digital silence.
static bool has_signal(const int16_t* pcm, size_t samples)
{
for (size_t i = 0; i < samples; i++) {
if (pcm[i] > 500 || pcm[i] < -500)
return true;
}
return false;
}
static void test_http(void)
{
printf("HTTP WAV sink\n");
int fd = connect_local(HTTP_PORT);
if (fd < 0) {
check(false, "connect to the HTTP sink");
return;
}
const char* req = "GET /audio.wav HTTP/1.1\r\nHost: tv\r\n\r\n";
check(write(fd, req, strlen(req)) == (ssize_t)strlen(req), "send the request");
// Headers end at the blank line; read a byte at a time so we do not eat
// into the WAV header that follows.
char headers[1024];
size_t hlen = 0;
bool complete = false;
while (hlen < sizeof(headers) - 1) {
if (!read_exact(fd, headers + hlen, 1))
break;
hlen++;
headers[hlen] = '\0';
if (hlen >= 4 && memcmp(headers + hlen - 4, "\r\n\r\n", 4) == 0) {
complete = true;
break;
}
}
check(complete, "receive complete HTTP headers");
check(strstr(headers, "200 OK") != NULL, "status line is 200 OK");
check(strstr(headers, "Content-Type: audio/wav") != NULL, "content type is audio/wav");
uint8_t wav[44];
if (!read_exact(fd, wav, sizeof(wav))) {
check(false, "receive the WAV header");
close(fd);
return;
}
check(memcmp(wav, "RIFF", 4) == 0, "RIFF magic");
check(memcmp(wav + 8, "WAVE", 4) == 0, "WAVE magic");
check(memcmp(wav + 12, "fmt ", 4) == 0, "fmt chunk");
check(rd_u32le(wav + 16) == 16, "fmt chunk length is 16");
check(rd_u16le(wav + 20) == 1, "format is PCM");
check(rd_u16le(wav + 22) == 2, "2 channels");
check(rd_u32le(wav + 24) == 48000, "48000 Hz");
check(rd_u32le(wav + 28) == 48000 * 4, "byte rate matches");
check(rd_u16le(wav + 32) == 4, "block align is 4");
check(rd_u16le(wav + 34) == 16, "16 bits per sample");
check(memcmp(wav + 36, "data", 4) == 0, "data chunk");
check(rd_u32le(wav + 4) == 0xFFFFFFFFu, "RIFF size is the unknown-length marker");
check(rd_u32le(wav + 40) == 0xFFFFFFFFu, "data size is the unknown-length marker");
int16_t pcm[4096];
bool got = read_exact(fd, pcm, sizeof(pcm));
check(got, "receive 16 KB of audio");
check(got && has_signal(pcm, sizeof(pcm) / sizeof(pcm[0])), "audio is not silence");
close(fd);
}
static void test_tcp(void)
{
printf("Raw PCM TCP sink\n");
int fd = connect_local(TCP_PORT);
if (fd < 0) {
check(false, "connect to the TCP sink");
return;
}
int16_t pcm[4096];
bool got = read_exact(fd, pcm, sizeof(pcm));
check(got, "receive 16 KB of audio");
check(got && has_signal(pcm, sizeof(pcm) / sizeof(pcm[0])), "audio is not silence");
close(fd);
}
static void test_status(engine_t* e)
{
printf("Status document\n");
json_writer_t w;
jw_init(&w);
jw_obj_open(&w, NULL);
engine_write_status(e, &w);
jw_obj_close(&w);
char* text = jw_take(&w);
check(text != NULL, "status serialises");
if (!text)
return;
json_value_t* v = json_parse(text);
check(v != NULL, "status is valid JSON");
if (v) {
check(strcmp(json_str(v, "state", ""), "running") == 0, "state is running");
const json_value_t* cap = json_get(v, "capture");
check(strcmp(json_str(cap, "backend", ""), "tone") == 0, "backend is the tone generator");
check(json_int(cap, "rate", 0) == 48000, "reports 48000 Hz");
check(json_int(cap, "frames", 0) > 0, "frames have been captured");
const json_value_t* levels = json_get(v, "levels");
check(json_num(levels, "peak", 0) > 0.05, "peak level is non-trivial");
check(json_len(json_get(levels, "bands")) == DSP_BANDS, "all bands reported");
const json_value_t* sinks = json_get(v, "sinks");
check(json_len(sinks) == 2, "two sinks reported");
for (size_t i = 0; i < json_len(sinks); i++) {
const json_value_t* s = json_at(sinks, i);
char label[64];
snprintf(label, sizeof(label), "sink '%s' started cleanly", json_str(s, "id", "?"));
check(json_bool(s, "ok", false), label);
}
json_free(v);
}
free(text);
}
int main(void)
{
log_init(LOG_WARN); // keep the test output readable
char cfg_text[512];
snprintf(cfg_text, sizeof(cfg_text),
"{\"capture\":{\"backend\":\"tone\",\"rate\":48000,\"channels\":2},"
"\"sinks\":[\"tcp\",\"http\"],"
"\"tcp\":{\"port\":%d},"
"\"http\":{\"port\":%d}}",
TCP_PORT, HTTP_PORT);
json_value_t* cfg = json_parse(cfg_text);
if (!cfg) {
fprintf(stderr, "test bug: config does not parse\n");
return 1;
}
engine_t* e = engine_create(NULL, NULL);
if (!e) {
fprintf(stderr, "cannot create the engine\n");
return 1;
}
char err[256] = { 0 };
printf("Engine\n");
if (!engine_start(e, cfg, err, sizeof(err))) {
printf(" FAIL start: %s\n", err);
return 1;
}
// Startup happens on the engine thread; wait for it to settle.
for (int i = 0; i < 100 && engine_state(e) == ENGINE_STARTING; i++)
sleep_ms(50);
check(engine_state(e) == ENGINE_RUNNING, "engine reaches the running state");
if (engine_state(e) != ENGINE_RUNNING) {
engine_destroy(e);
json_free(cfg);
return 1;
}
test_tcp();
test_http();
test_status(e);
engine_stop(e);
check(engine_state(e) == ENGINE_STOPPED, "engine stops cleanly");
engine_destroy(e);
json_free(cfg);
printf("\n%s\n", failures ? "FAILED" : "All engine checks passed.");
return failures ? 1 : 0;
}