1085 lines
51 KiB
C
1085 lines
51 KiB
C
#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>
|
||
#include <sys/stat.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);
|
||
}
|
||
|
||
/* 带自定义 settings.conf 的启动(P2 蓝牙:测 audio.output_bt / bt.codec 读取生效) */
|
||
static int start_audiod_settings(const char *sink, const char *settings_path)
|
||
{
|
||
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 (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
|
||
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
|
||
_exit(127);
|
||
}
|
||
return pid;
|
||
}
|
||
|
||
/* 带 amixer 追踪的启动(A3/DSD:断言「音量双组写」PCM+DSD_VOLUME 成对下发)。settings_path 可空。 */
|
||
static int start_audiod_trace(const char *sink, const char *settings_path, const char *trace_path)
|
||
{
|
||
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 (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
|
||
if (trace_path) setenv("MEOW_AUDIO_AMIXER_TRACE", trace_path, 1);
|
||
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
|
||
_exit(127);
|
||
}
|
||
return pid;
|
||
}
|
||
|
||
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
||
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, 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 = flags;
|
||
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;
|
||
}
|
||
/* 直接客户端 open */
|
||
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
|
||
{
|
||
return do_open_f(c, rate, ch, fmt, 0, ack);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* 字节保真校验(A3 高清 / DoP 透传):WAV 头 rate/ch/bits 精确匹配 + data 段与 payload 逐字节一致。
|
||
* 返回 0=合法且逐字节等同;-1=文件/头非法;-2=data_len 或内容不匹配。 */
|
||
static int wav_byte_exact(const char *path, int rate_expect, int ch_expect, int bits_expect,
|
||
const uint8_t *payload, size_t plen)
|
||
{
|
||
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, data_len, byterate;
|
||
memcpy(&ch, h + 22, 2); memcpy(&rate, h + 24, 4); memcpy(&byterate, h + 28, 4); memcpy(&bits, h + 34, 2);
|
||
memcpy(&data_len, h + 40, 4);
|
||
if (ch != (uint16_t)ch_expect || rate != (uint32_t)rate_expect || bits != (uint16_t)bits_expect) { fclose(f); return -1; }
|
||
if (byterate != rate * ch * bits / 8) { fclose(f); return -1; }
|
||
if (data_len != (uint32_t)plen) { fclose(f); return -2; }
|
||
uint8_t *body = malloc(plen ? plen : 1);
|
||
if (!body) { fclose(f); return -1; }
|
||
size_t rd = fread(body, 1, plen, f);
|
||
fclose(f);
|
||
int ok = (rd == plen) && (memcmp(body, payload, plen) == 0);
|
||
free(body);
|
||
return ok ? 0 : -2;
|
||
}
|
||
|
||
/* 等 WAV 头出现(设备已 dev_open 写入 44B 头):**STOP 前调用**——修复 OPEN→write→STOP 过快时
|
||
* 播放线程尚未来得及 reconfigure(设备未打开,STOP 的 session_end 清 reconfig → 空 WAV)的竞态。
|
||
* 返回 0=头已出现;-1=超时。 */
|
||
static int wait_wav_open(const char *path, int tmo_ms)
|
||
{
|
||
for (int i = 0; i < tmo_ms / 50; i++) {
|
||
struct stat st;
|
||
if (stat(path, &st) < 0) { usleep(50000); continue; }
|
||
if (st.st_size >= 44) return 0;
|
||
usleep(50000);
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
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)");
|
||
|
||
/* ---- T11:P2 蓝牙 输出目标(OUT_BT)+ 音量 AVRCP 隔离 + 格式归一 + codec。
|
||
* 宿主以 FILE sink 跑(真 bluealsa 传输留给真机),验证目标选择/音量隔离/格式逻辑:
|
||
* - settings.conf `audio.output_bt=1` + `bt.codec=SBC` → target=bt / codec=SBC / volctl=avrcp / grant fmt=S16;
|
||
* - BT 下 MSG_AUDIO_VOLUME 设置:**不下发 DAC 音量反转**——g_vol_pct 不变、不持久化 audio.volume、
|
||
* volctl 仍 avrcp(隔离成立);
|
||
* - MSG_AUDIO_OUTPUT 切回 DAC:target=dac / volctl=dac;此后音量设置写 DAC + 持久化 audio.volume。
|
||
* 断言:BT 路径绝不动 DAC 音量(plan §三/§五「两套隔离」);BT 格式归一=S16;codec 读到生效。 ---- */
|
||
printf("[T11 BT output target + AVRCP vol isolation]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
const char *tconf = "/tmp/meow_test_settings.conf";
|
||
FILE *tf = fopen(tconf, "w");
|
||
if (tf) { fprintf(tf, "# meow-settings 1\naudio.volume = 60\naudio.output_bt = 1\nbt.codec = SBC\n"); fclose(tf); }
|
||
g_audiod_pid = start_audiod_settings("file", tconf);
|
||
usleep(400000);
|
||
struct cli b11;
|
||
check(cli_connect(&b11) == 0, "connect B11");
|
||
m_audio_open_ack_t ack11;
|
||
check(do_open(&b11, 44100, 2, AUDIO_FMT_S16, &ack11) == 0 && ack11.err == MEOW_OK, "open B11 (bt)");
|
||
char q11[1024];
|
||
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "target=bt"),
|
||
"settings output_bt=1 -> target=bt");
|
||
check(strstr(q11, "codec=SBC") != NULL, "settings bt.codec=SBC -> codec=SBC");
|
||
check(strstr(q11, "volctl=avrcp") != NULL, "bt -> volctl=avrcp (AVRCP vol control)");
|
||
check(strstr(q11, "fmt=S16") != NULL, "bt grant fmt=S16 (format normalize)");
|
||
/* BT/file sink 写一段流(应正常落盘到 WAV) */
|
||
do_write(&b11, pcm, (uint32_t)gen_sine(pcm, sizeof pcm, (int)(ack11.grant_fmt ? ack11.grant_fmt : AUDIO_FMT_S16), 600, 440, 44100));
|
||
usleep(150000);
|
||
/* 音量设置 while BT:不下发 DAC——g_vol_pct 不变(60)、不持久化 audio.volume=40、volctl 仍 avrcp */
|
||
{
|
||
int32_t pct = 40;
|
||
if (sf(b11.fd, MSG_AUDIO_VOLUME, 8, &pct, sizeof pct) == 0) {
|
||
uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab); /* 消费 ACK */
|
||
}
|
||
usleep(100000);
|
||
}
|
||
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "volctl=avrcp"),
|
||
"vol set while bt -> volctl still avrcp (isolation)");
|
||
check(strstr(q11, "vol=60") != NULL, "vol set while bt -> DAC vol unchanged (60)");
|
||
{
|
||
FILE *chk = fopen(tconf, "r"); int has40 = 0; char l[128];
|
||
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||
check(!has40, "bt vol set does NOT persist audio.volume=40");
|
||
}
|
||
/* MSG_AUDIO_OUTPUT 切回 DAC */
|
||
{
|
||
m_audio_output_t oo; oo.target = AUDIO_OUT_DAC;
|
||
if (sf(b11.fd, MSG_AUDIO_OUTPUT, 9, &oo, sizeof oo) == 0) {
|
||
uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab);
|
||
}
|
||
usleep(150000);
|
||
}
|
||
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "target=dac"),
|
||
"MSG_AUDIO_OUTPUT -> target=dac");
|
||
check(strstr(q11, "volctl=dac") != NULL, "dac -> volctl=dac");
|
||
/* 现在音量设置应写 DAC + 持久化 */
|
||
{
|
||
int32_t pct = 40;
|
||
if (sf(b11.fd, MSG_AUDIO_VOLUME, 10, &pct, sizeof pct) == 0) {
|
||
uint8_t ab[64]; uint16_t at; rf(b11.fd, &at, ab, sizeof ab);
|
||
}
|
||
usleep(150000);
|
||
}
|
||
check(do_query(&b11, q11, sizeof q11) == 0 && strstr(q11, "vol=40"), "dac vol set -> g_vol_pct=40");
|
||
{
|
||
FILE *chk = fopen(tconf, "r"); int has40 = 0; char l[128];
|
||
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||
check(has40, "dac vol set persists audio.volume=40");
|
||
}
|
||
do_ctrl(&b11, MSG_AUDIO_STOP, ack11.stream_id);
|
||
close(b11.fd);
|
||
stop_audiod();
|
||
|
||
/* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。
|
||
* ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧,
|
||
* 验证 WAV 为 32-bit 且 data 段与 payload 逐字节相等(无降位/无重排)。
|
||
* ② S24@88200(32-in-32):grant S24,WAV 32-bit 逐字节等同。鉴真机 DAC 高清率由 hw_probe 决定。 */
|
||
printf("[T12 24/32bit hi-res byte-exact]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
g_audiod_pid = start_audiod(); /* file sink */
|
||
usleep(400000);
|
||
{
|
||
struct cli h12;
|
||
check(cli_connect(&h12) == 0, "connect H12");
|
||
m_audio_open_ack_t ack12;
|
||
check(do_open_f(&h12, 192000, 2, AUDIO_FMT_S32, 0, &ack12) == 0 && ack12.err == MEOW_OK,
|
||
"open S32@192000");
|
||
check(ack12.grant_fmt == AUDIO_FMT_S32, "grant S32 for S32 request (FILE byte-exact)");
|
||
check(ack12.grant_rate == 192000, "grant_rate=192000 (no SRC)");
|
||
uint8_t pl[4096];
|
||
size_t fs12 = 2 * 4; /* stereo S32 */
|
||
size_t nframes = sizeof pl / fs12;
|
||
uint8_t *op = pl;
|
||
for (size_t i = 0; i < nframes; i++) {
|
||
uint32_t l = (uint32_t)(((i * 7u) & 0xFFFFu) << 16);
|
||
uint32_t r = (uint32_t)((0x10000u - ((i * 7u) & 0xFFFFu)) << 16);
|
||
memcpy(op, &l, 4); op += 4; memcpy(op, &r, 4); op += 4;
|
||
}
|
||
do_write(&h12, pl, (uint32_t)sizeof pl);
|
||
char q12[1024];
|
||
do_query(&h12, q12, sizeof q12); /* 活动态快照 + 给播放线程时间重配设备 */
|
||
check(strstr(q12, "fmt=S32") != NULL, "query fmt=S32 (hi-res format exposed)");
|
||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||
do_ctrl(&h12, MSG_AUDIO_STOP, ack12.stream_id);
|
||
for (int i = 0; i < 300; i++) { /* ≤30s 等排空 + WAV 头回填 */
|
||
usleep(100000);
|
||
if (wav_byte_exact(TWAV, 192000, 2, 32, pl, sizeof pl) == 0) break;
|
||
}
|
||
check(wav_byte_exact(TWAV, 192000, 2, 32, pl, sizeof pl) == 0,
|
||
"S32@192000 WAV byte-exact (32-bit, no downshift)");
|
||
/* ② S24@88200(32-in-32) */
|
||
unlink(TWAV);
|
||
struct cli h12b;
|
||
check(cli_connect(&h12b) == 0, "connect H12b");
|
||
m_audio_open_ack_t ack12b;
|
||
check(do_open_f(&h12b, 88200, 2, AUDIO_FMT_S24, 0, &ack12b) == 0 && ack12b.err == MEOW_OK,
|
||
"open S24@88200");
|
||
check(ack12b.grant_fmt == AUDIO_FMT_S24, "grant S24 for S24 request (32-in-32)");
|
||
uint8_t pl2[4096];
|
||
size_t fs24 = 2 * 4;
|
||
size_t n2 = sizeof pl2 / fs24;
|
||
uint8_t *op2 = pl2;
|
||
for (size_t i = 0; i < n2; i++) {
|
||
uint32_t l = (uint32_t)((i * 11u) << 8); /* 24-bit 区在 [31:8] */
|
||
uint32_t r = (uint32_t)(0x1000000u - ((i * 11u) << 8));
|
||
memcpy(op2, &l, 4); op2 += 4; memcpy(op2, &r, 4); op2 += 4;
|
||
}
|
||
do_write(&h12b, pl2, (uint32_t)sizeof pl2);
|
||
char q12b[1024];
|
||
do_query(&h12b, q12b, sizeof q12b); /* 活动态快照:fmt 未清 */
|
||
check(strstr(q12b, "fmt=S24") != NULL, "query fmt=S24 (hi-res format exposed)");
|
||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||
do_ctrl(&h12b, MSG_AUDIO_STOP, ack12b.stream_id);
|
||
for (int i = 0; i < 300; i++) {
|
||
usleep(100000);
|
||
if (wav_byte_exact(TWAV, 88200, 2, 32, pl2, sizeof pl2) == 0) break;
|
||
}
|
||
check(wav_byte_exact(TWAV, 88200, 2, 32, pl2, sizeof pl2) == 0,
|
||
"S24@88200 WAV byte-exact (32-in-32)");
|
||
close(h12.fd); close(h12b.fd);
|
||
}
|
||
stop_audiod();
|
||
|
||
/* ---- T13:DoP DSD 透传(A3+DSD:fmt=DOP+flags=DSD,audiod 只透传;不做 dsd2pcm)。
|
||
* 写已知 DoP 帧(32-bit:bits[31:16]=16bit DSD,bits[15:8]=标记 0xFA(L)/0x05(R)),
|
||
* 验证 grant=DOP、WAV 32-bit 逐字节等同(透传保真)、query fmt=DoP dsd=1。 */
|
||
printf("[T13 DoP DSD passthrough]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
g_audiod_pid = start_audiod();
|
||
usleep(400000);
|
||
{
|
||
struct cli d13;
|
||
check(cli_connect(&d13) == 0, "connect D13");
|
||
m_audio_open_ack_t ack13;
|
||
check(do_open_f(&d13, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack13) == 0 && ack13.err == MEOW_OK,
|
||
"open DoP@176400 (DSD64)");
|
||
check(ack13.grant_fmt == AUDIO_FMT_DOP, "grant=DoP (DOP passthrough)");
|
||
check(ack13.grant_rate == 176400, "grant_rate=176400 (container rate, no SRC)");
|
||
/* DoP 帧:16bit DSD 数据(左/右不同)+ 标记字节 */
|
||
uint8_t dop[4096];
|
||
size_t fsd = 2 * 4;
|
||
size_t nd = sizeof dop / fsd;
|
||
uint8_t *od = dop;
|
||
for (size_t i = 0; i < nd; i++) {
|
||
uint32_t wl = ((uint32_t)(i & 0xFFFFu) << 16) | (0xFAu << 8); /* 标记 0xFA=左 */
|
||
uint32_t wr = ((uint32_t)(~i & 0xFFFFu) << 16) | (0x05u << 8); /* 标记 0x05=右 */
|
||
memcpy(od, &wl, 4); od += 4; memcpy(od, &wr, 4); od += 4;
|
||
}
|
||
do_write(&d13, dop, (uint32_t)sizeof dop);
|
||
char q13[1024];
|
||
do_query(&d13, q13, sizeof q13); /* 活动态快照:fmt=DoP/dsd=1 */
|
||
check(strstr(q13, "fmt=DoP") != NULL, "query fmt=DoP");
|
||
check(strstr(q13, "dsd=1") != NULL, "query dsd=1 (DoP stream active)");
|
||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||
do_ctrl(&d13, MSG_AUDIO_STOP, ack13.stream_id);
|
||
for (int i = 0; i < 300; i++) {
|
||
usleep(100000);
|
||
if (wav_byte_exact(TWAV, 176400, 2, 32, dop, sizeof dop) == 0) break;
|
||
}
|
||
check(wav_byte_exact(TWAV, 176400, 2, 32, dop, sizeof dop) == 0,
|
||
"DoP WAV byte-exact (32-bit container, marker preserved)");
|
||
/* 无 DSD 标志的 DOP 请求 → 拒(INVAL):DoP 必须带 AUDIO_FLAG_DSD */
|
||
struct cli d13b;
|
||
check(cli_connect(&d13b) == 0, "connect D13b");
|
||
m_audio_open_ack_t ack13b;
|
||
check(do_open_f(&d13b, 176400, 2, AUDIO_FMT_DOP, 0, &ack13b) == 0 && ack13b.err == MEOW_ERR_INVAL,
|
||
"DoP without AUDIO_FLAG_DSD rejected (INVAL)");
|
||
close(d13.fd); close(d13b.fd);
|
||
}
|
||
stop_audiod();
|
||
|
||
/* ---- T14:DSD 失败回退 = 报错关流(案 A;不引 dsd2pcm、不静默退化)。
|
||
* output_bt=1(BT 目标)下 DoP open → MEOW_ERR_NODEV(BT/A2DP 不能承载 DoP);
|
||
* 此后普通 PCM(S16)open 仍正常(BT 目标不变),证明失败只关该 DSD 流、不殃及目标。 */
|
||
printf("[T14 DSD fallback (BT) = error/close]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
const char *tconf14 = "/tmp/meow_test_settings14.conf";
|
||
FILE *tf14 = fopen(tconf14, "w");
|
||
if (tf14) { fprintf(tf14, "# meow-settings 1\naudio.volume = 60\naudio.output_bt = 1\n"); fclose(tf14); }
|
||
g_audiod_pid = start_audiod_settings("file", tconf14);
|
||
usleep(400000);
|
||
{
|
||
struct cli d14;
|
||
check(cli_connect(&d14) == 0, "connect D14");
|
||
m_audio_open_ack_t ack14;
|
||
check(do_open_f(&d14, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack14) == 0 &&
|
||
ack14.err == MEOW_ERR_NODEV, "DoP on BT target rejected (NODEV)");
|
||
/* 失败后 state 归干净(无流基线);同目标普通 PCM 正常开 */
|
||
struct cli d14b;
|
||
check(cli_connect(&d14b) == 0, "connect D14b");
|
||
m_audio_open_ack_t ack14b;
|
||
check(do_open(&d14b, 44100, 2, AUDIO_FMT_S16, &ack14b) == 0 && ack14b.err == MEOW_OK,
|
||
"PCM open still ok on BT after DSD reject");
|
||
do_ctrl(&d14b, MSG_AUDIO_STOP, ack14b.stream_id);
|
||
close(d14.fd); close(d14b.fd);
|
||
}
|
||
stop_audiod();
|
||
|
||
/* ---- T15:音量收编 v2 双组写 + 权威(A3:audiod 唯一音量权威;泛化双组写 PCM+DSD_VOLUME;
|
||
* 维持平方锥度;audio.volume 由 audiod 读写权威)。amixer 轨迹断言:一次 vol set 同发 numid=1/2(DSD)。
|
||
* 全部经 MSG_AUDIO_VOLUME —— 权威路径;host FILE sink 无真实 amixer(popen 失败仍记 trace)。 */
|
||
printf("[T15 volume v2 dual-group write + authority]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
const char *tconf15 = "/tmp/meow_test_settings15.conf";
|
||
FILE *tf15 = fopen(tconf15, "w");
|
||
if (tf15) { fprintf(tf15, "# meow-settings 1\naudio.volume = 60\n"); fclose(tf15); }
|
||
const char *trace15 = "/tmp/meow_amixer_trace15.txt";
|
||
unlink(trace15);
|
||
g_audiod_pid = start_audiod_trace("file", tconf15, trace15);
|
||
usleep(400000);
|
||
{
|
||
struct cli v15;
|
||
check(cli_connect(&v15) == 0, "connect V15");
|
||
m_audio_open_ack_t ack15;
|
||
check(do_open(&v15, 44100, 2, AUDIO_FMT_S16, &ack15) == 0 && ack15.err == MEOW_OK, "open V15");
|
||
/* 权威写音量(非 BT→DAC):设置 40 */
|
||
{
|
||
int32_t pct = 40;
|
||
if (sf(v15.fd, MSG_AUDIO_VOLUME, 15, &pct, sizeof pct) == 0) {
|
||
uint8_t ab[64]; uint16_t at; rf(v15.fd, &at, ab, sizeof ab);
|
||
}
|
||
}
|
||
usleep(150000);
|
||
char q15[1024];
|
||
check(do_query(&v15, q15, sizeof q15) == 0 && strstr(q15, "vol=40"),
|
||
"authority vol set -> g_vol_pct=40 (audiod authoritative)");
|
||
/* 持久化 audio.volume(audiod 读写权威,与 launcher/watchdog 同源同键) */
|
||
{
|
||
FILE *chk = fopen(tconf15, "r"); int has40 = 0; char l[128];
|
||
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||
check(has40, "audio.volume persisted = 40 (audiod authority)");
|
||
}
|
||
/* 双组写断言:trace 含 numid=1、numid=2、且 DSD_VOLUME(11) 同值(40→锥度≈40×40×192/10000≈30) */
|
||
{
|
||
char trb[4096]; int trl = 0;
|
||
int fd = open(trace15, O_RDONLY);
|
||
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||
check(trl > 0 && strstr(trb, "cset numid=1 ") && strstr(trb, "cset numid=2 ") &&
|
||
strstr(trb, "cset numid=11 "), "dual-group write: PCM numid=1/2 + DSD_VOLUME numid=11");
|
||
/* 平方锥度:pct=40 → dac = (100-40)^2*192/10000 = 69.12 → 69(±1 容差) */
|
||
int dac40 = (100 - 40) * (100 - 40) * 192 / 10000;
|
||
/* 从 trace 找 numid=2 的值,应与 dac40 一致 */
|
||
const char *p2 = strstr(trb, "cset numid=2 ");
|
||
int got2 = p2 ? atoi(p2 + strlen("cset numid=2 ")) : -1;
|
||
check(got2 == dac40, "square taper kept (numid=2 == (100-40)^2*192/10000)");
|
||
}
|
||
do_ctrl(&v15, MSG_AUDIO_STOP, ack15.stream_id);
|
||
close(v15.fd);
|
||
}
|
||
stop_audiod();
|
||
|
||
/* ---- T16:监控日志记录高清/DoP 实际率 + 格式(A3:monitor 记录实际率;DSD 标记)。
|
||
* 生命周期同 T8:流活跃期写 /tmp/audio_monitor_<bootts>.log,行含 %uHz/%uch/DoP 与 hwrate/vol。 */
|
||
printf("[T16 monitor logs hi-res/DoP rate+fmt]\n");
|
||
unlink(TSOCK); unlink(TWAV);
|
||
g_audiod_pid = start_audiod();
|
||
usleep(400000);
|
||
{
|
||
struct cli m16;
|
||
check(cli_connect(&m16) == 0, "connect M16");
|
||
/* 先清空 monitor 文件(复用同一 bootts 名;先前测试 T8-T15 已写入大量旧行),
|
||
* 保证下面的检查只针对本 DoP 流新写行(append 模式将在文件尾追加)。 */
|
||
char bootts16[64] = "";
|
||
{ int fd = open("/tmp/meow_bootts", O_RDONLY); if (fd >= 0) { ssize_t r = read(fd, bootts16, 63); close(fd); if (r > 0) bootts16[r] = 0; } }
|
||
char mp16[160]; snprintf(mp16, sizeof mp16, "/tmp/audio_monitor_%s.log", bootts16);
|
||
unlink(mp16);
|
||
m_audio_open_ack_t ack16;
|
||
check(do_open_f(&m16, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack16) == 0 && ack16.err == MEOW_OK,
|
||
"open DoP@176400 for monitor");
|
||
uint8_t dop16[4096];
|
||
size_t fs16 = 2 * 4;
|
||
size_t nd16 = sizeof dop16 / fs16;
|
||
uint8_t *od16 = dop16;
|
||
for (size_t i = 0; i < nd16; i++) {
|
||
uint32_t wl = ((uint32_t)(i & 0xFFFFu) << 16) | (0xFAu << 8);
|
||
uint32_t wr = ((uint32_t)(~i & 0xFFFFu) << 16) | (0x05u << 8);
|
||
memcpy(od16, &wl, 4); od16 += 4; memcpy(od16, &wr, 4); od16 += 4;
|
||
}
|
||
for (int i = 0; i < 12; i++) do_write(&m16, dop16, (uint32_t)sizeof dop16);
|
||
char mb16[4096]; int ml16 = 0;
|
||
for (int i = 0; i < 30; i++) {
|
||
usleep(200000); /* 等 ≥2 个 1s 节拍写 monitor 行 */
|
||
int fd = open(mp16, O_RDONLY);
|
||
if (fd >= 0) { ml16 = (int)read(fd, mb16, sizeof mb16 - 1); close(fd); if (ml16 > 0) mb16[ml16] = 0; }
|
||
if (ml16 > 0 && strstr(mb16, "176400Hz/2ch/DoP") && strstr(mb16, "dsd=1")) break;
|
||
}
|
||
check(ml16 > 0 && strstr(mb16, "176400Hz/2ch/DoP") && strstr(mb16, "dsd=1"),
|
||
"monitor log records container rate 176400 + fmt DoP + dsd=1");
|
||
do_ctrl(&m16, MSG_AUDIO_STOP, ack16.stream_id);
|
||
close(m16.fd);
|
||
}
|
||
stop_audiod();
|
||
|
||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||
return g_fail ? 1 : 0;
|
||
} |