#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= / * MEOW_AUDIO_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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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_.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"); } } stop_audiod(); printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail); return g_fail ? 1 : 0; }