Files
MeowOS-For-R3P2/patch_v2/meow_audiod/test_audiod_host.c
T
AskaEth 3d70a3dd2c feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘)
- OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复
- ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640
- 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫)
- 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
2026-09-03 12:16:01 +08:00

696 lines
31 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#define _GNU_SOURCE
/* test_audiod_host.c — meow_audiod 宿主集成测试(FILE sink,无 /dev/snd 环境)。
*
* 验证 AUDIOD_DESIGN.md 里程碑 A1:
* - 全 IPC 链路:OPEN→WRITE→QUERY→PAUSE/RESUME→STOP→EOF 回收
* - 两个客户端抢占:旧流收 MSG_AUDIO_PREEMPTED(PCM 丢弃)
* - tone 生成 WAV 可核验正弦(440Hz,帧数/过零率粗检)
*
* 启动被测试 audiod(宿主二进制,env MEOW_AUDIO_SINK=file / MEOW_AUDIO_OUT=<wav> /
* MEOW_AUDIO_SOCK=<sock> / MEOW_AUDIO_FORCE=1 跳过门控)。
*
* 编译(宿主):cc -I../meow_launcherd -Os -Wall -Wextra test_audiod_host.c -o test_audiod_host
*/
#include "meow_ipc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#define TSOCK "/tmp/meow_audio_test.sock"
#define TWAV "/tmp/meow_audio_test.wav"
static int g_audiod_pid = -1;
static int g_pass = 0, g_fail = 0;
static void check(int cond, const char *name)
{
if (cond) { printf(" PASS %s\n", name); g_pass++; }
else { printf(" FAIL %s\n", name); g_fail++; }
}
/* ---- 原始 IPC 客户端(全有界:connect 非阻塞 2s + SO_SNDTIMEO 2s——回归卡死时
* 测试 FAIL 而不是跟着挂死) ---- */
struct cli { int fd; };
static int cli_connect(struct cli *c)
{
c->fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (c->fd < 0) return -1;
int fl = fcntl(c->fd, F_GETFL, 0);
if (fl >= 0) fcntl(c->fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", TSOCK);
int r = connect(c->fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = c->fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, 2000) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1;
}
}
if (r < 0) { close(c->fd); return -1; }
if (fl >= 0) fcntl(c->fd, F_SETFL, fl);
struct timeval tv;
tv.tv_sec = 2; tv.tv_usec = 0;
setsockopt(c->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
return 0;
}
static int w_all(int fd, const void *p, size_t n)
{
const uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
return 0;
}
static int r_all(int fd, void *p, size_t n)
{
uint8_t *b = p; size_t o = 0;
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r <= 0) return -1; o += (size_t)r; }
return 0;
}
static int sf(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &type, 2); memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &len, 4);
if (w_all(fd, hdr, sizeof hdr)) return -1;
if (len && pl && w_all(fd, pl, len)) return -1;
return 0;
}
static int rf(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
if (r_all(fd, hdr, sizeof hdr)) return -1;
uint32_t magic, len; uint16_t ver;
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2); memcpy(&len, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
if (len && r_all(fd, rbuf, len)) return -1;
return (int)len;
}
/* 带超时读一帧(poll) */
static int rf_tmo(int fd, int tmo_ms, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
{
struct pollfd p; p.fd = fd; p.events = POLLIN; p.revents = 0;
if (poll(&p, 1, tmo_ms) <= 0) return -2;
return rf(fd, type, rbuf, rcap);
}
static void stop_audiod(void)
{
if (g_audiod_pid > 0) { kill(g_audiod_pid, SIGTERM); waitpid(g_audiod_pid, NULL, 0); g_audiod_pid = -1; }
}
/* 带超时的停止:SIGTERM 后最多等 tmo_ms;不退 → SIGKILL 并返回 0(卡死回归显形)。 */
static int stop_audiod_tmo(int tmo_ms)
{
if (g_audiod_pid <= 0) return 1;
kill(g_audiod_pid, SIGTERM);
int ok = 0;
for (int i = 0; i < tmo_ms / 50; i++) {
if (waitpid(g_audiod_pid, NULL, WNOHANG) == g_audiod_pid) { ok = 1; break; }
usleep(50000);
}
if (!ok) { kill(g_audiod_pid, SIGKILL); waitpid(g_audiod_pid, NULL, 0); }
g_audiod_pid = -1;
return ok;
}
static int start_audiod_ex(const char *sink, const char *wedge_after, const char *ring_wait_ms)
{
pid_t pid = fork();
if (pid == 0) {
setenv("MEOW_AUDIO_SINK", sink, 1);
setenv("MEOW_AUDIO_OUT", TWAV, 1);
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
setenv("MEOW_AUDIO_FORCE", "1", 1);
if (wedge_after) setenv("MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES", wedge_after, 1);
if (ring_wait_ms) setenv("MEOW_AUDIO_RING_WAIT_MS", ring_wait_ms, 1);
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
_exit(127);
}
return pid;
}
static int start_audiod(void)
{
return start_audiod_ex("file", NULL, NULL);
}
/* 直接客户端 open */
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
{
m_audio_open_t o; memset(&o, 0, sizeof o);
snprintf(o.app_id, sizeof o.app_id, "test");
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = 0;
if (sf(c->fd, MSG_AUDIO_OPEN, 1, &o, sizeof o)) return -1;
uint8_t rbuf[256]; uint16_t rt;
if (rf(c->fd, &rt, rbuf, sizeof rbuf) < 0) return -1;
if (rt != MSG_AUDIO_OPEN_ACK) return -2;
memcpy(ack, rbuf, sizeof *ack);
return 0;
}
static int do_write(struct cli *c, const void *pcm, uint32_t len)
{
return sf(c->fd, MSG_AUDIO_WRITE, 0, pcm, len);
}
static int do_ctrl(struct cli *c, uint16_t type, uint32_t sid)
{
m_audio_stream_t s; s.stream_id = sid;
if (sf(c->fd, type, 2, &s, sizeof s)) return -1;
uint8_t rbuf[64]; uint16_t rt;
if (rf(c->fd, &rt, rbuf, sizeof rbuf) < 0) return -1;
return (rt == MSG_ACK) ? 0 : -2;
}
static int do_query(struct cli *c, char *out, size_t n)
{
if (sf(c->fd, MSG_AUDIO_QUERY, 3, NULL, 0)) return -1;
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rt;
int r = rf(c->fd, &rt, rbuf, sizeof rbuf);
if (r < 0 || rt != MSG_AUDIO_STATE) return -1;
size_t m = ((size_t)r < n - 1) ? (size_t)r : (n - 1);
memcpy(out, rbuf, m);
out[m] = 0;
return 0;
}
/* 生成正弦 PCM(S16 or S32,stereo 交织),返回字节数 */
static size_t gen_sine(uint8_t *buf, size_t cap, int fmt, int frames, double freq, int rate)
{
int bps = (fmt == AUDIO_FMT_S16) ? 2 : 4;
size_t fs = 2; /* 双声道 */
size_t n = (frames > (int)(cap / (fs * bps))) ? (cap / (fs * bps)) : (size_t)frames;
size_t o = 0;
for (size_t i = 0; i < n; i++) {
double ph = 2.0 * M_PI * freq * (double)i / (double)rate;
int16_t s = (int16_t)(sin(ph) * 0.3 * 32767);
for (int c = 0; c < 2; c++) {
int32_t v = (fmt == AUDIO_FMT_S16) ? (int32_t)s : ((int32_t)s << 16);
if (fmt == AUDIO_FMT_S16) { memcpy(buf + o, &s, 2); o += 2; }
else { memcpy(buf + o, &v, 4); o += 4; }
}
}
return o;
}
/* ---- WAV 校验:合法 + 近似正弦(逐帧过零/幅度粗检) ---- */
static int verify_wav(const char *path, int rate_expect, int ch_expect, int bits_expect)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
unsigned char h[44];
if (fread(h, 1, 44, f) != 44) { fclose(f); return -1; }
if (memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4) || memcmp(h + 12, "fmt ", 4)) { fclose(f); return -1; }
uint16_t ch, bits; uint32_t rate, byterate;
memcpy(&ch, h + 22, 2); memcpy(&rate, h + 24, 4); memcpy(&byterate, h + 28, 4); memcpy(&bits, h + 34, 2);
if (ch != ch_expect || rate != (uint32_t)rate_expect || bits != bits_expect) { fclose(f); return -1; }
if (byterate != rate * ch * bits / 8) { fclose(f); return -1; }
uint32_t data_len; memcpy(&data_len, h + 40, 4);
if (data_len < 44) { fclose(f); return -1; }
/* 读「左声道」样本(跳右声道)统计过零比例:正弦 freq@rate 单声道期望过零比例 ≈ 2*freq/rate。
* 仅对「近似正弦」做粗检(40% 容差),确认不是静音/噪声/满幅方波。 */
int bytes_samp = bits / 8;
int frame_bytes = ch * bytes_samp;
uint8_t framebuf[8];
long zero_cross = 0, total = 0, prev_neg = 0;
for (long i = 0; i < (long)(data_len / frame_bytes); i++) {
if (fread(framebuf, 1, (size_t)frame_bytes, f) != (size_t)frame_bytes) break;
int32_t val = 0;
if (bytes_samp == 2) { int16_t v; memcpy(&v, framebuf, 2); val = v; }
else { int32_t v; memcpy(&v, framebuf, 4); val = v >> 16; }
if (val != 0) {
int neg = val < 0;
if (total > 0 && neg != prev_neg) zero_cross++;
prev_neg = neg;
}
total++;
}
fclose(f);
if (total < 100) return -3;
double obs = (double)zero_cross / (double)total;
double exp = 2.0 * 440.0 / (double)rate_expect;
if (obs < exp * 0.6 || obs > exp * 1.4) return -2;
return 0;
}
int main(void)
{
unlink(TSOCK); unlink(TWAV);
g_audiod_pid = start_audiod();
if (g_audiod_pid < 0) { printf("cannot fork audiod\n"); return 1; }
usleep(300000);
printf("=== meow_audiod host test (FILE sink) ===\n");
/* ---- T1:全链路 OPEN→WRITE→QUERY→PAUSE/RESUME→STOP ---- */
printf("[T1 full chain]\n");
struct cli a;
check(cli_connect(&a) == 0, "connect A");
m_audio_open_ack_t ack;
check(do_open(&a, 44100, 2, AUDIO_FMT_S16, &ack) == 0 && ack.err == MEOW_OK && ack.stream_id > 0, "open A");
printf(" grant=%u/%u/%s sid=%u\n", (unsigned)ack.grant_rate, (unsigned)ack.grant_ch, (ack.grant_fmt == AUDIO_FMT_S16 ? "S16" : "S32"), (unsigned)ack.stream_id);
uint8_t pcm[4096];
size_t wb = gen_sine(pcm, sizeof pcm, (int)(ack.grant_fmt ? ack.grant_fmt : AUDIO_FMT_S16), 100, 440, 44100);
check(wb > 0 && do_write(&a, pcm, (uint32_t)wb) == 0, "write A");
usleep(150000);
char q[1024];
check(do_query(&a, q, sizeof q) == 0 && strstr(q, "active=1"), "query active");
printf(" query: %s\n", q);
check(do_ctrl(&a, MSG_AUDIO_PAUSE, ack.stream_id) == 0, "pause A");
usleep(50000);
do_query(&a, q, sizeof q);
check(strstr(q, "paused=1") != NULL, "query paused");
check(do_ctrl(&a, MSG_AUDIO_RESUME, ack.stream_id) == 0, "resume A");
usleep(50000);
check(do_ctrl(&a, MSG_AUDIO_STOP, ack.stream_id) == 0, "stop A");
usleep(100000);
do_query(&a, q, sizeof q);
check(strstr(q, "active=0") != NULL, "stop -> inactive");
close(a.fd);
/* ---- T2:两个客户端抢占(旧收 PREEMPTED,PCM 丢弃) ---- */
printf("[T2 preemption]\n");
struct cli a2, b2;
check(cli_connect(&a2) == 0, "connect A2");
m_audio_open_ack_t ackA2;
check(do_open(&a2, 48000, 2, AUDIO_FMT_S16, &ackA2) == 0 && ackA2.err == MEOW_OK, "open A2");
size_t wb2 = gen_sine(pcm, sizeof pcm, (int)(ackA2.grant_fmt ? ackA2.grant_fmt : AUDIO_FMT_S16), 100, 440, 48000);
do_write(&a2, pcm, (uint32_t)wb2);
check(cli_connect(&b2) == 0, "connect B2");
m_audio_open_ack_t ackB2;
check(do_open(&b2, 96000, 2, AUDIO_FMT_S16, &ackB2) == 0 && ackB2.err == MEOW_OK, "open B2 (preempt)");
/* A2 应收到 PREEMPTED */
uint8_t rbuf[128]; uint16_t rt;
int npr = rf_tmo(a2.fd, 1000, &rt, rbuf, sizeof rbuf);
check(npr >= (int)sizeof(m_audio_preempted_t) && rt == MSG_AUDIO_PREEMPTED, "A2 got PREEMPTED");
if (npr >= (int)sizeof(m_audio_preempted_t)) {
m_audio_preempted_t pm; memcpy(&pm, rbuf, sizeof pm);
printf(" preempted by %s (stream %u)\n", pm.by_app, (unsigned)pm.stream_id);
}
/* A2 写被丢弃;B2 写生效 */
do_write(&a2, pcm, (uint32_t)wb2); /* 应丢弃(A2 已 draining) */
usleep(100000);
do_query(&a2, q, sizeof q);
printf(" after preempt, A2 query: %s\n", q);
do_write(&b2, pcm, (uint32_t)wb2);
usleep(100000);
do_query(&b2, q, sizeof q);
check(strstr(q, "active=1") && strstr(q, "rate=96000"), "B2 active after preempt");
do_ctrl(&b2, MSG_AUDIO_STOP, ackB2.stream_id);
do_ctrl(&a2, MSG_AUDIO_STOP, ackA2.stream_id);
close(a2.fd); close(b2.fd);
/* ---- T3:EOF 回收(客户端直接断链) ---- */
printf("[T3 EOF recycle]\n");
struct cli c3;
check(cli_connect(&c3) == 0, "connect C3");
m_audio_open_ack_t ackC3;
check(do_open(&c3, 44100, 2, AUDIO_FMT_S16, &ackC3) == 0, "open C3");
/* 不 STOP,直接断链(close fd)→ audiod poll 读到 EOF → 回收 */
do_write(&c3, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, AUDIO_FMT_S16, 100, 440, 44100));
close(c3.fd);
usleep(200000);
struct cli c3q; check(cli_connect(&c3q) == 0, "connect C3q");
do_query(&c3q, q, sizeof q);
printf(" after EOF: %s\n", q);
check(strstr(q, "active=0") != NULL, "EOF recycle -> inactive");
close(c3q.fd);
/* ---- T5:长流回归(3 分钟等效 PCM 持续灌;FILE sink 全速排空 → 加速时钟等效)。
* 回归目标:真机「长流卡死」——喂流期间 listener/IO 循环必须持续可服务,
* 帧数零丢失、零 underrun、零丢弃,STOP 后排空落盘完整。 ---- */
printf("[T5 long stream (3min equivalent)]\n");
struct cli l5;
check(cli_connect(&l5) == 0, "connect L5");
m_audio_open_ack_t ackL5;
check(do_open(&l5, 44100, 2, AUDIO_FMT_S16, &ackL5) == 0 && ackL5.err == MEOW_OK, "open L5");
uint32_t fs5 = (uint32_t)ackL5.grant_ch * (ackL5.grant_fmt == AUDIO_FMT_S16 ? 2u : 4u);
if (fs5 == 0) fs5 = 4;
uint64_t total_frames = 44100ULL * 180; /* 3 分钟等效流 */
uint64_t total_bytes = total_frames * fs5;
uint8_t chunk[4096];
memset(chunk, 0x5A, sizeof chunk); /* 内容无关:测完整性/不卡死 */
size_t per = (sizeof chunk / fs5) * fs5;
uint64_t sent = 0;
int werr = 0, probe_n = 0, probe_ok = 0;
while (sent < total_bytes) {
uint64_t w = total_bytes - sent;
if (w > per) w = per;
if (do_write(&l5, chunk, (uint32_t)w) != 0) { werr = 1; break; }
sent += w;
if (sent % (512 * per) == 0) { /* 每 2MB 探一次 listener 活性 */
struct cli s5;
s5.fd = -1;
probe_n++;
if (cli_connect(&s5) == 0) {
char q5[1024];
if (do_query(&s5, q5, sizeof q5) == 0 && strstr(q5, "active=1")) probe_ok++;
close(s5.fd);
}
}
}
check(!werr && sent == total_bytes, "long feed completed (io never stalled)");
printf(" fed %llu bytes (%llu frames), mid-feed status probes %d/%d alive\n",
(unsigned long long)sent, (unsigned long long)(sent / fs5), probe_ok, probe_n);
check(probe_n >= 10 && probe_ok == probe_n, "listener/io alive during long feed");
do_ctrl(&l5, MSG_AUDIO_STOP, ackL5.stream_id);
/* 排空证明改为「WAV 写满 + STATE 归零」双判据:流结束后 played 已由 session_clean 清零
* (会话回收契约),不再作排空判据;WAV size==44+fed 才是「帧无丢失」的 ground truth。 */
long wavsz5 = -1;
char q5[1024];
int clean5 = 0;
for (int i = 0; i < 300; i++) { /* 最多 30s 等排空+回填 WAV 头 */
usleep(100000);
do_query(&l5, q5, sizeof q5);
FILE *f = fopen(TWAV, "rb");
if (f) { fseek(f, 0, SEEK_END); wavsz5 = ftell(f); fclose(f); }
if (wavsz5 == (long)(44 + total_bytes) && strstr(q5, "active=0") && strstr(q5, "played=0")) {
clean5 = 1; break;
}
}
check(clean5, "long stream fully played (WAV==fed bytes, STATE clean)");
check(strstr(q5, "underrun=0") != NULL && strstr(q5, "dropped=0") != NULL,
"no underrun/drop on long stream");
check(wavsz5 == (long)(44 + total_bytes), "long WAV size == 44 + fed bytes");
close(l5.fd);
/* ---- T4:tone 写 WAV(由 CLI 走完整 IPC 链路;mipsel/host CLI 共用) ---- */
printf("[T4 tone -> WAV]\n");
unlink(TWAV);
pid_t tone_pid = fork();
if (tone_pid == 0) {
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
execl("./meow_audio_host", "meow_audio_host", "tone", "440", "500", "44100", (char *)0);
_exit(127);
}
waitpid(tone_pid, NULL, 0);
usleep(300000);
check(verify_wav(TWAV, 44100, 2, 16) == 0, "WAV valid sine");
printf(" wav size: ");
{ FILE *f = fopen(TWAV, "rb"); if (f) { fseek(f, 0, SEEK_END); printf("%ld bytes\n", ftell(f)); fclose(f); } }
stop_audiod();
/* ---- T6:设备 wedge 生存(故障注入复现真机长流卡死场景)。
* MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES=128KB → 播放线程永久堵在 dev_write(睡到 g_stop)。
* 旧代码:ring 满 → IO 线程 ring_write cond_wait 永不醒 → accept 停摆 → 全链冻结。
* 护栏后:ring 写有界(RING_WAIT_MS=50ms)超时丢弃,IO/accept/preempt 全部存活。 ---- */
printf("[T6 device wedge survival]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", "131072", "50");
usleep(400000);
struct cli w6;
check(cli_connect(&w6) == 0, "connect W6");
m_audio_open_ack_t ackW6;
check(do_open(&w6, 44100, 2, AUDIO_FMT_S16, &ackW6) == 0 && ackW6.err == MEOW_OK, "open W6");
int werr6 = 0;
uint64_t sent6 = 0;
for (int i = 0; i < 100; i++) { /* 400KB:128KB 后 wedge,其余全靠丢弃 */
if (do_write(&w6, chunk, (uint32_t)per) != 0) { werr6 = 1; break; }
sent6 += per;
}
check(!werr6 && sent6 == 100 * (uint64_t)per, "feed continues across wedge (no deadlock)");
struct cli s6;
s6.fd = -1;
char q6[1024];
int io_alive = 0;
if (cli_connect(&s6) == 0) {
if (do_query(&s6, q6, sizeof q6) == 0 && strstr(q6, "route_mode=")) io_alive = 1;
close(s6.fd);
}
check(io_alive, "io loop alive after wedge (connect+query served)");
check(io_alive && strstr(q6, "dropped=") != NULL && atoi(strstr(q6, "dropped=") + 8) > 0,
"drop policy engaged (dropped>0)");
struct cli p6;
p6.fd = -1;
int pre6 = 0;
if (cli_connect(&p6) == 0) {
m_audio_open_ack_t ak6;
if (do_open(&p6, 48000, 2, AUDIO_FMT_S16, &ak6) == 0 && ak6.err == MEOW_OK) pre6 = 1;
close(p6.fd);
}
check(pre6, "preempt OPEN granted while wedged");
close(w6.fd);
check(stop_audiod_tmo(5000) != 0, "wedged audiod exits on SIGTERM within 5s");
/* ---- T7:慢/不读客户端下行丢弃(server→client 写绝不堵 IO 循环)。
* Z 狂发 QUERY 从不读响应 → server 下行缓冲灌满 → send_frame 单发即丢(txdrop++)。
* 护栏断言:server 不卡死、新连接照常服务、txdrop 有计数。 ---- */
printf("[T7 slow-client tx drop]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", NULL, NULL);
usleep(400000);
struct cli z7;
check(cli_connect(&z7) == 0, "connect Z7");
int serr7 = 0, sent7 = 0;
for (int i = 0; i < 2000; i++) {
if (sf(z7.fd, MSG_AUDIO_QUERY, 9, NULL, 0) != 0) { serr7 = 1; break; }
sent7++;
}
check(!serr7 && sent7 == 2000, "query spam sent (client never reads)");
struct cli f7;
f7.fd = -1;
char q7[1024];
int alive7 = 0;
if (cli_connect(&f7) == 0) {
if (do_query(&f7, q7, sizeof q7) == 0 && strstr(q7, "route_mode=")) alive7 = 1;
close(f7.fd);
}
check(alive7, "server responsive after tx drops");
check(alive7 && strstr(q7, "txdrop=") != NULL && atoi(strstr(q7, "txdrop=") + 7) > 0,
"txdrop counted");
close(z7.fd);
stop_audiod();
/* ---- T8:数字域电平表 + 全面监控日志(M2.97 DAC 实时监控)。
* 已知幅度正弦(amp=0.3, 440Hz):理论 RMS=0.3/√2 → -13.5dBFS,peak=0.3 → -10.5dBFS。
* audiod 电平表为整数近似(octave 线性插值,误差 ~0.5dB)→ 容差 ±2.5dB。
* 监控日志:流活跃期主线程 1s 节拍写 /tmp/audio_monitor_<bootts>.log(宿主无卡降级)。 ---- */
printf("[T8 level meter + monitor log]\n");
unlink(TSOCK); unlink(TWAV);
g_audiod_pid = start_audiod(); /* file sink */
usleep(400000);
struct cli m8;
check(cli_connect(&m8) == 0, "connect M8");
m_audio_open_ack_t ackM8;
check(do_open(&m8, 44100, 2, AUDIO_FMT_S16, &ackM8) == 0 && ackM8.err == MEOW_OK, "open M8");
/* 喂 ~270ms 的 440Hz/0.3 幅度正弦(>2 个 100ms 窗口) */
for (int i = 0; i < 12; i++)
do_write(&m8, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, AUDIO_FMT_S16, 1000, 440, 44100));
char q8[1024];
check(do_query(&m8, q8, sizeof q8) == 0 && strstr(q8, "rms=") && strstr(q8, "peak="),
"query exposes rms/peak keys");
int rms8 = -990, pk8 = -990;
for (int i = 0; i < 20; i++) { /* 最多 2s 等首个窗口结算(播放排空竞态护栏) */
do_query(&m8, q8, sizeof q8);
rms8 = atoi(strstr(q8, "rms=") + 4);
pk8 = atoi(strstr(q8, "peak=") + 5);
if (rms8 > -990) break;
usleep(100000);
}
printf(" sine 0.3: rms=%d peak=%d (expect rms≈-135 peak≈-105 deci-dB)\n", rms8, pk8);
check(rms8 >= -160 && rms8 <= -110, "sine RMS ≈ -13.5dBFS (±2.5dB)");
check(pk8 >= -130 && pk8 <= -80, "sine peak ≈ -10.5dBFS (±2.5dB)");
/* 静音 → 地板(轮询等窗口结算:窗口按块粒度对齐,消除播放排空时序竞态) */
{
uint8_t zeros[4096];
memset(zeros, 0, sizeof zeros);
for (int i = 0; i < 8; i++) do_write(&m8, zeros, sizeof zeros); /* ~186ms */
int floor_ok = 0;
for (int i = 0; i < 20; i++) { /* 最多 2s 等纯零窗口结算 */
usleep(100000);
do_query(&m8, q8, sizeof q8);
rms8 = atoi(strstr(q8, "rms=") + 4);
pk8 = atoi(strstr(q8, "peak=") + 5);
if (rms8 <= -990 && pk8 <= -990) { floor_ok = 1; break; }
}
printf(" silence: rms=%d peak=%d\n", rms8, pk8);
check(floor_ok, "silence -> -inf floor");
}
/* monitor CLI 冒烟(流仍活跃:输出含电平字段+电平条) */
{
unlink("/tmp/meow_mon_cli.out");
pid_t mp = fork();
if (mp == 0) {
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
int fd = open("/tmp/meow_mon_cli.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { dup2(fd, 1); close(fd); }
execl("./meow_audio_host", "meow_audio_host", "monitor", "2", (char *)0);
_exit(127);
}
int mst = 0;
waitpid(mp, &mst, 0);
char mb[4096];
int ml = 0;
int fd = open("/tmp/meow_mon_cli.out", O_RDONLY);
if (fd >= 0) { ml = (int)read(fd, mb, sizeof mb - 1); close(fd); if (ml > 0) mb[ml] = 0; }
check(ml > 0 && strstr(mb, "rms=") && strstr(mb, "LIVE") && strchr(mb, '['),
"monitor CLI prints live level bar");
}
/* 监控日志文件:活跃期应已写入(前面耗时已超 2 个 1s 节拍) */
char bootts[64] = "";
{
int fd = open("/tmp/meow_bootts", O_RDONLY);
if (fd >= 0) { ssize_t r = read(fd, bootts, 63); close(fd); if (r > 0) bootts[r] = 0; }
}
char monpath[160];
snprintf(monpath, sizeof monpath, "/tmp/audio_monitor_%s.log", bootts);
char monbuf[4096];
int monlen = 0;
{
int fd = open(monpath, O_RDONLY);
if (fd >= 0) { monlen = (int)read(fd, monbuf, sizeof monbuf - 1); close(fd); if (monlen > 0) monbuf[monlen] = 0; }
}
check(monlen > 0 && strstr(monbuf, "app=test") && strstr(monbuf, "rms=") &&
strstr(monbuf, "hwrate=") && strstr(monbuf, "vol="),
"monitor log file written while stream active");
/* 流停止即停写 */
do_ctrl(&m8, MSG_AUDIO_STOP, ackM8.stream_id);
usleep(1400000);
off_t msz1 = -1, msz2 = -1;
{ int fd = open(monpath, O_RDONLY); if (fd >= 0) { msz1 = lseek(fd, 0, SEEK_END); close(fd); } }
usleep(1400000);
{ int fd = open(monpath, O_RDONLY); if (fd >= 0) { msz2 = lseek(fd, 0, SEEK_END); close(fd); } }
check(msz1 >= 0 && msz1 == msz2, "monitor log stops when stream stops");
close(m8.fd);
stop_audiod();
/* ---- T9:孤儿会话回收(客户端崩溃/被杀 → 会话完整回收 → STATE 归零)。
* 复现 ncm 被内核 OOM 杀(sig=9)场景:客户端子进程 OPEN + 写正弦(使 written/played/rms/peak/
* lvlseq 非零、流活跃),随即 SIGKILL 子进程(内核自动关 socket fd)——audiod 应检测 EOF 并完整
* 回收:active=0/sid=0/written=0/played=0/underrun=0/rms=peak=-inf/lvlseq=0,STATE 回干净无流基线。 ---- */
printf("[T9 orphan reclaim (client SIGKILL)]\n");
unlink(TSOCK); unlink(TWAV);
g_audiod_pid = start_audiod(); /* file sink */
usleep(400000);
{
int p9[2];
if (pipe(p9) == 0) {
pid_t kid = fork();
if (kid == 0) {
/* 子进程 = 客户端:OPEN + 写足够正弦(≥2 窗口,使电平表结算)→ 通知父进程后挂死 */
close(p9[0]);
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
struct cli c9;
if (cli_connect(&c9) != 0) _exit(2);
m_audio_open_ack_t ack9;
if (do_open(&c9, 44100, 2, AUDIO_FMT_S16, &ack9) != 0 || ack9.err != MEOW_OK) _exit(3);
uint8_t pc9[4096];
for (int i = 0; i < 12; i++) {
size_t w9 = gen_sine(pc9, sizeof pc9, AUDIO_FMT_S16, 1000, 440, 44100);
if (do_write(&c9, pc9, (uint32_t)w9) != 0) _exit(4);
}
char ok = 1;
if (write(p9[1], &ok, 1) != 1) _exit(5);
for (;;) pause(); /* 等父进程 SIGKILL(模拟 OOM sig=9) */
}
close(p9[1]);
char ok = 0;
int got = (int)read(p9[0], &ok, 1); /* 等子进程写完 */
close(p9[0]);
check(got == 1 && ok == 1, "child client wrote audio (stream active)");
/* pre-death:流活跃 + 电平表已污染(证明待回收的会话有非零统计) */
int pre9 = 0;
char q9b[1024] = "";
for (int i = 0; i < 20; i++) { /* 最多 2s 等电平窗口结算 */
struct cli q9c;
if (cli_connect(&q9c) != 0) { usleep(100000); continue; }
if (do_query(&q9c, q9b, sizeof q9b) == 0) {
int rms9 = atoi(strstr(q9b, "rms=") + 4);
int lvl9 = atoi(strstr(q9b, "lvlseq=") + 7);
if (strstr(q9b, "active=1") && rms9 > -990 && lvl9 > 0) { pre9 = 1; close(q9c.fd); break; }
}
close(q9c.fd);
usleep(100000);
}
printf(" pre-crash: %s", q9b);
check(pre9, "stream active & meter populated before crash");
/* 模拟崩溃:SIGKILL 子进程(内核关 socket fd → audiod poll 到 EOF → 回收) */
kill(kid, SIGKILL);
waitpid(kid, NULL, 0);
/* post-death:轮询直到 STATE 完全归零(排空后 session_clean) */
int clean9 = 0;
char q9a[1024] = "";
for (int i = 0; i < 40; i++) { /* 最多 4s */
usleep(100000);
struct cli q9c;
if (cli_connect(&q9c) != 0) continue;
if (do_query(&q9c, q9a, sizeof q9a) == 0) {
int rr = atoi(strstr(q9a, "rms=") + 4);
int pp = atoi(strstr(q9a, "peak=") + 5);
int lv = atoi(strstr(q9a, "lvlseq=") + 7);
if (strstr(q9a, "active=0") && strstr(q9a, "sid=0") &&
strstr(q9a, "written=0") && strstr(q9a, "played=0") &&
strstr(q9a, "underrun=0") && rr <= -990 && pp <= -990 && lv == 0) {
clean9 = 1; close(q9c.fd); break;
}
}
close(q9c.fd);
}
printf(" post-crash: %s", q9a);
check(clean9, "SIGKILL orphan -> STATE fully clean (sid/written/played/rms/peak/lvlseq zeroed)");
check(strstr(q9a, "active=0") != NULL && strstr(q9a, "app=-") != NULL,
"orphan query shows no-stream baseline");
} else {
check(0, "pipe for T9 child");
}
}
/* ---- T10:连接表溢出丢弃护栏与回收(回归 fd=N accept 风暴,§2026-09-02 真机 1489 次)。
* 背景:CLI_MAX=8(§12「客户端全有界」护栏)。ncm「每次 audio_out_open 新建一条短连接」
* (旧 audio_out_sync_volume)在边下边播/切歌 churn 下会反复占用连接槽:8 张表占满后,
* 新 connect 的 accept 因 cli_alloc 失败被**安全丢弃**(日志 "client limit reached, drop"),
* 表回收后 NEW 连接照常服务。本用例:
* ① 洪泛 connect→write→close ×60(远超 CLI_MAX=8,走上述 drop 护栏);
* ② 断言 audiod **不 wedge/不崩**(此后 SIGTERM 干净退出);
* ③ 等表回收(短连接已 close,EOF 逐 poll 回收)后,新 OPEN 仍被授权(fd 复用回收)。
* 「同一 fd=N 被反复 accept」= 对端短连接 + fd 低位复用,本是**正常回收机理**,非循环缺陷;
* 根因在对端每 open 建新连接,由 ncm side audio_out.c「音量同步每进程一次」消除(vol_sync_sim)。 ---- */
printf("[T10 client-table overflow guard & recycle]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", NULL, NULL);
usleep(400000);
int burst = 0;
for (int i = 0; i < 60; i++) { /* 洪泛 > CLI_MAX=8 */
struct cli v;
if (cli_connect(&v) == 0) {
int32_t pct = 50;
if (sf(v.fd, MSG_AUDIO_VOLUME, 7, &pct, sizeof pct) == 0) burst++;
close(v.fd); /* 对端读 EOF → cli_free 回收 */
}
}
printf(" issued=%d/60 (cap by CLI_MAX=8 overflow drops)\n", burst);
check(burst > 0, "connections issued (overflow hits drop guard)");
/* ③ 等表回收(≤1s:EOF 逐 poll 回收)后新 OPEN 仍被授权 */
usleep(700000);
int eok = 0;
{
struct cli e;
if (cli_connect(&e) == 0) {
m_audio_open_ack_t ak;
if (do_open(&e, 48000, 2, AUDIO_FMT_S16, &ak) == 0 && ak.err == MEOW_OK) eok = 1;
close(e.fd);
}
}
check(eok, "fresh OPEN after overflow (table recycled, no exhaustion/wedge)");
check(stop_audiod_tmo(5000) != 0, "audiod not wedged by overflow (SIGTERM clean exit)");
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}