From 6b17a2ac7a74685d25b038795d3edd0b56b969bd Mon Sep 17 00:00:00 2001 From: MeowOS Date: Mon, 31 Aug 2026 22:40:08 +0800 Subject: [PATCH] =?UTF-8?q?A1:=20meow=5Faudiod=20=E7=8B=AC=E5=8D=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E6=9C=8D=E5=8A=A1=EF=BC=88agent-244=20?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=20+=20agent-246=20=E7=9C=9F=E6=9C=BA?= =?UTF-8?q?=E6=94=B6=E5=B0=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - meow_audiod:启动门控(/tmp/meow_boot_mode 非 meow exit 0)、IO/播放/路由三线程、 ring ~200ms、ALSA UAPI 独占直开、切率 mute-重配-恢复、抢占(后开赢+PREEMPTED)、 socket EOF 回收、xrun PREPARE 恢复;meow_route 轮询逻辑并入路由线程 - meow_audio CLI:status/tone/route/vol;services.d/audiod.conf(trigger=manual,on-fail) - SDK meow_ui_audio.c 编进 meow_ui.a:懒连接/读线程分发 PREEMPTED 回调/EPIPE 标死/ 无 audiod 返回 MEOW_ERR_NOENT 不隐式直开(MEOW_AUDIO_DIRECT=1 仅调试) - launcher:确认 MeowOS 模式后 meow_svcd start audiod(double-fork best-effort 不阻塞) - meow_route 退役:S89 预置换 audiod.conf + 清理旧 meow_route.conf(防拉起已删二进制 重启循环);fw_content.conf 删 meow_route、加 meow_audiod+meow_audio(md5 打包时填) - 真机定位并修复 ALSA UAPI 两大坑(即 ncm 播放无声根因): ① hw_params 须全放行初始化(mask 全 1/interval [0,UINT_MAX]),memset 0 interval 等价只能取 0 → refine 必空 → HW_PARAMS 恒 EINVAL ② access/format mask 先清零再单点(pmask 只 OR),全 1 被 choose 成 MMAP_INTERLEAVED → write() 恒 EINVAL - 附带修复:dev_write fd<0 防日志风暴丢 PCM 限频日志;SDK state 解析 app_id 截空白; meow_ui.h 文档负错误码表述修正 - 验证:宿主 FILE sink 全 IPC 链+抢占+tone WAV 20/20,SDK 客户端 13/13; qemu-mipsel 同过;真机 tone 44.1k/96k 切率 written=played、underrun=0、日志完整 - meow_ui.a 53a3edd2 → ncm 重链 d2031829(已热更到卡);launcher a2f932a1 --- build_sdk.sh | 2 +- meow_ui.h | 46 ++++ meow_ui_audio.c | 515 +++++++++++++++++++++++++++++++++++++++++++ test_ui_audio_host | Bin 0 -> 74112 bytes test_ui_audio_host.c | 158 +++++++++++++ 5 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 meow_ui_audio.c create mode 100755 test_ui_audio_host create mode 100644 test_ui_audio_host.c diff --git a/build_sdk.sh b/build_sdk.sh index 3c6090b..8c67ac6 100755 --- a/build_sdk.sh +++ b/build_sdk.sh @@ -10,7 +10,7 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media stb_image" +OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image" CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" diff --git a/meow_ui.h b/meow_ui.h index a7acd98..62a5f61 100644 --- a/meow_ui.h +++ b/meow_ui.h @@ -870,6 +870,52 @@ int meow_font_set_tier(int tier); /* 当前字体档(初始 = MEOW_FONT_TIER_COMMON;meow_ui_init 时读 MEOW_FONT_TIER env 对齐)。 */ int meow_font_tier(void); +/* ============================================================ */ +/* §11 独占音频客户端(devdocs/plan/AUDIOD_DESIGN.md §7;meow_ui_audio.c) */ +/* ============================================================ */ +/* 说明:App 只经本 API 喂 PCM 给 meow_audiod(独占 hw:0,0 守护,svcd 托管)。audiod 不在一律 + * 失败(open 返回 MEOW_ERR_NOENT,负值),不做隐式直开回退;调试可用 env MEOW_AUDIO_DIRECT=1 走 + * 自带直接 ALSA 直开路径(非正式能力,勿依赖)。 + * 生命周期:open 取 stream_id>0 → write 喂 PCM(阻塞,socket 背压)→ pause/resume → close 释放。 + * 被抢占(后开流赢):SDK 内部读线程收 MSG_AUDIO_PREEMPTED → 调 on_preempt 回调 + 标写死 + * (后续 write 返回负值,App 应重新 open)。 */ + +/* 当前流快照(meow_audio_query 填充)。 */ +typedef struct meow_audio_state { + int active; /* 1=audiod 有活跃流(不一定是本 App 的) */ + char app_id[32]; /* 活跃流 app_id(无流为 "-") */ + uint32_t stream_id; /* 活跃流 id */ + uint32_t rate; /* 活跃流采样率 */ + uint16_t ch; /* 活跃流声道数 */ + uint16_t fmt; /* 活跃流格式(AUDIO_FMT_*) */ + uint64_t written_frames; /* 客户端写入 ring 的帧 */ + uint64_t played_frames; /* 播放线程写到设备的帧 */ + uint32_t underrun; /* xrun 计数 */ + int paused; /* 当前流是否暂停 */ + int route_port; /* 当前路由口(-1 未知) */ + int vol; /* 当前音量 0-100 */ +} meow_audio_state; + +/* 被抢占回调:by_app=抢占者 app_id(非空)。ctx 由 on_preempt 注册时传入。 */ +typedef void (*meow_audio_preempt_fn)(const char *by_app, void *ctx); + +/* 开流。rate/ch/fmt=App 声源原生参数(fmt 值见 meow_ipc.h AUDIO_FMT_*)。 + * 返回 stream_id(>0);负错误码(MEOW_ERR_NOENT=audiod 不在;MEOW_ERR_INVAL=参数非法; + * MEOW_ERR_NODEV=无可用 PCM 设备;MEOW_ERR_* 常量本身即负值)。open 后客户端须按内部记录的 + * grant 格式产出 PCM。 */ +int meow_audio_open(int rate, int ch, int fmt); +/* 写 PCM。pcm=native fmt/ch 样本(open 传的参数),frames=帧数;内部归一成 grant 并分批发送。 + * 返回写入帧数;<0=流已死(被抢占/audiod 断开),App 应重新 open。阻塞(socket 背压)。 */ +int meow_audio_write(const void *pcm, int frames); +/* 暂停(1)/恢复(0)当前流。返回 0 成功;<0 失败。 */ +int meow_audio_pause(int sid, int paused); +/* 关流(STOP + 释放)。返回 0;连接保留供下次 open。 */ +int meow_audio_close(int sid); +/* 查询 audiod 快照(填充 out)。返回 0 成功;<0 失败。 */ +int meow_audio_query(meow_audio_state *out); +/* 注册被抢占回调(可选;内部读线程分发)。重复调用幂等。 */ +void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx); + #ifdef __cplusplus } #endif diff --git a/meow_ui_audio.c b/meow_ui_audio.c new file mode 100644 index 0000000..e16afb8 --- /dev/null +++ b/meow_ui_audio.c @@ -0,0 +1,515 @@ +/* meow_ui_audio.c — App 侧独占音频客户端(SDK 公共件;见 meow_ui.h §11)。 + * + * 定位:App 只经本 API 喂 PCM 给 meow_audiod(独占 hw:0,0 守护)。audiod 不在一律失败 + * (open 返回 MEOW_ERR_NOENT 等负错误码),不做隐式直开回退;调试可 env MEOW_AUDIO_DIRECT=1 走自带 + * 直接 ALSA 直开(非正式能力)。 + * + * 传输:AF_UNIX SOCK_STREAM 到 audiod socket(缺省 /usr/data/meow_audio.sock;env + * MEOW_AUDIO_SOCK 覆盖,宿主/qemu)。帧格式复用 meow_ipc.h 16B 头 + m_audio_* 负载(同一份)。 + * + * 线程模型(懒连 + 内部读线程): + * - 首次 open/query 时 connect → 懒启动读线程(poll 读 audiod 下发帧)。 + * - 读线程为唯一读方:PREEMPTED → 调 on_preempt 回调 + 标写死;OPEN_ACK/STATE/ACK(匹配 + * 当前挂起请求)→ 拷贝到 g_resp、唤醒调用方(条件变量)。 + * - 写路径(write/pause/resume/close)由 App 线程在另一方向写同一 fd(全双工,互不干扰)。 + * + * 格式归一:open 返回前记录 grant_rate/grant_ch/grant_fmt;meow_audio_write 把 native(open 参数) + * 样本逐帧转成 grant 格式(mono→stereo 复制 / S16↔S32 定值升降位),分批 ≤4096B 发 MSG_AUDIO_WRITE。 + * 无 SRC:grant_rate == native rate(audiod 把 DAC 原生率设为流率)。 + */ +#include "meow_ui.h" +#include "meow_ipc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#define UI_AUDIO_HAVE_ALSA 1 +#endif + +#define AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock" + +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER; +static int g_fd = -1; +static pthread_t g_thr; +static int g_thr_on = 0; +static volatile int g_quit = 0; + +/* 当前流状态 */ +static int g_has_stream = 0; +static uint32_t g_sid = 0; +static int g_dead = 0; /* 收到 PREEMPTED 后置 1:write 返回负值 */ +static int g_own_rate = 0, g_own_ch = 0, g_own_fmt = 0; /* native(open 参数) */ +static int g_grant_rate = 0, g_grant_ch = 0, g_grant_fmt = 0; + +static meow_audio_preempt_fn g_preempt_cb = NULL; +static void *g_preempt_ctx = NULL; + +/* 挂起请求(读线程分发;一次只有一个) */ +static uint32_t g_pending = 0; /* req_id,0=无挂起 */ +static uint16_t g_resp_type = 0; +static uint8_t g_resp[MEOW_IPC_MAX]; +static uint32_t g_resp_len = 0; + +/* 调试直开(env MEOW_AUDIO_DIRECT=1) */ +static int g_direct = 0; +static int g_dir_fd = -1; + +static const char *sock_path(void) +{ + const char *e = getenv("MEOW_AUDIO_SOCK"); + return (e && e[0]) ? e : AUD_SOCK_DEFAULT; +} + +static int connect_path(const char *path) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, path, sizeof a.sun_path - 1); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + return fd; +} + +static int write_all(int fd, const uint8_t *p, size_t n) +{ + size_t off = 0; + while (off < n) { + ssize_t w = write(fd, p + off, n - off); + if (w < 0) { if (errno == EINTR) continue; return -1; } + if (w == 0) return -1; + off += (size_t)w; + } + return 0; +} +static int read_all(int fd, uint8_t *p, size_t n) +{ + size_t off = 0; + while (off < n) { + ssize_t r = read(fd, p + off, n - off); + if (r < 0) { if (errno == EINTR) continue; return -1; } + if (r == 0) return -1; + off += (size_t)r; + } + return 0; +} +static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len) +{ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t magic = MEOW_IPC_MAGIC; + uint16_t ver = MEOW_IPC_VERSION; + memcpy(hdr + 0, &magic, 4); + memcpy(hdr + 4, &ver, 2); + memcpy(hdr + 6, &type, 2); + memcpy(hdr + 8, &req_id, 4); + memcpy(hdr + 12, &len, 4); + if (write_all(fd, hdr, sizeof hdr) != 0) return -1; + if (len && payload && write_all(fd, payload, len) != 0) return -1; + return 0; +} +static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap) +{ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + if (read_all(fd, hdr, sizeof hdr) != 0) 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 > 0 && read_all(fd, rbuf, len) != 0) return -1; + return (int)len; +} + +static void *audio_thread_main(void *arg); + +/* (调用方已持 mu)标记断线并关闭。 */ +static void mark_dead_locked(void) +{ + if (g_fd >= 0) { close(g_fd); g_fd = -1; } + g_dead = 1; +} + +/* (调用方已持 mu)懒连接 + 懒起读线程。成功 0;audiod 不在 -1。 */ +static int conn_locked(void) +{ + if (g_fd >= 0) return g_fd; + if (g_direct) return -2; /* 调试直开:不连 socket */ + const char *sp = sock_path(); + int fd = connect_path(sp); + if (fd < 0) return -1; + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one); + g_fd = fd; + if (!g_thr_on) { g_quit = 0; g_thr_on = 1; pthread_create(&g_thr, NULL, audio_thread_main, NULL); pthread_detach(g_thr); } + return fd; +} + +/* ---- 调试直开(MEOW_AUDIO_DIRECT=1,仅开发) ---- */ +#ifdef UI_AUDIO_HAVE_ALSA +static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); } +static void pivset(struct snd_interval *iv, unsigned val) +{ + iv->min = iv->max = val; iv->integer = 1; iv->openmin = iv->openmax = 0; iv->empty = 0; +} +static int direct_alsa_open(int rate) +{ + int fd = open("/dev/snd/pcmC0D0p", O_RDWR); + if (fd < 0) return -1; + struct snd_pcm_hw_params p; + /* 「全放行」初始化(snd_pcm_hw_params_any 语义):memset 0 的 interval 会被内核当成 + * 「只能取 0」约束 → refine 必空 → HW_PARAMS 恒 EINVAL(audiod 同款坑,勿回退)。 */ + memset(&p, 0, sizeof p); + unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1; + for (unsigned i = 0; i < nm; i++) + for (unsigned b = 0; b < sizeof p.masks[i].bits / sizeof p.masks[i].bits[0]; b++) + p.masks[i].bits[b] = ~0u; + unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1; + for (unsigned i = 0; i < ni; i++) { p.intervals[i].min = 0; p.intervals[i].max = ~0u; } + p.flags = 0; + /* mask 先清零再单点(pmask 只 OR):全 1 的 access 会被内核 choose 成 MMAP_INTERLEAVED + * → write() EINVAL(audiod 同款第二坑,勿回退)。 */ + struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK]; + memset(am, 0, sizeof *am); pmask(am, SNDRV_PCM_ACCESS_RW_INTERLEAVED); + struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK]; + memset(fm, 0, sizeof *fm); pmask(fm, SNDRV_PCM_FORMAT_S16_LE); + struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK]; + memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD); + pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 2); + pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], (unsigned)rate); + p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0; + if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p) != 0) { close(fd); return -1; } + ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0); + return fd; +} +#endif + +/* ---- 格式转换 ---- */ +static int fmt_bps(int fmt) +{ + switch (fmt) { + case AUDIO_FMT_S16: return 2; + case AUDIO_FMT_S24: + case AUDIO_FMT_S32: + case AUDIO_FMT_FLOAT: return 4; + default: return 2; + } +} +static int32_t sample_from_native(const uint8_t *p, int fmt) +{ + if (fmt == AUDIO_FMT_S16) { int16_t v; memcpy(&v, p, 2); return v; } + int32_t v; memcpy(&v, p, 4); return v; /* S32/S24(32-in-32)/FLOAT 按 32 位采样 */ +} +static void sample_to_grant(uint8_t *p, int fmt, int32_t v) +{ + if (fmt == AUDIO_FMT_S16) { int16_t x = (int16_t)v; memcpy(p, &x, 2); return; } + memcpy(p, &v, 4); +} +static int32_t convert_sample(int32_t v, int nfmt, int gfmt) +{ + int nb = (nfmt == AUDIO_FMT_S16) ? 16 : 32; + int gb = (gfmt == AUDIO_FMT_S16) ? 16 : 32; + if (gb > nb) return v << (gb - nb); + if (gb < nb) return v >> (nb - gb); + return v; +} + +/* 发一批 WRITE(≤MEOW_IPC_MAX,帧对齐;调用方已持 mu) */ +static int send_write_chunk(const uint8_t *p, uint32_t len) +{ + if (g_fd < 0) { g_dead = 1; return -1; } + if (send_frame(g_fd, MSG_AUDIO_WRITE, 0, p, len) != 0) { mark_dead_locked(); return -1; } + return 0; +} + +/* ---- 公开 API ---- */ + +int meow_audio_open(int rate, int ch, int fmt) +{ + /* env 检查:调试直开 */ + const char *de = getenv("MEOW_AUDIO_DIRECT"); + g_direct = (de && de[0] == '1') ? 1 : 0; + + pthread_mutex_lock(&g_mu); + if (g_has_stream) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_BUSY; } + if (ch > 2 || ch < 1 || rate <= 0 || fmt < AUDIO_FMT_S16 || fmt > AUDIO_FMT_FLOAT || fmt == AUDIO_FMT_FLOAT) { + pthread_mutex_unlock(&g_mu); + return MEOW_ERR_INVAL; + } + g_own_rate = rate; g_own_ch = ch; g_own_fmt = fmt; + + if (g_direct) { +#ifdef UI_AUDIO_HAVE_ALSA + int fd = direct_alsa_open(rate); + if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NODEV; } + g_dir_fd = fd; + g_grant_rate = rate; g_grant_ch = 2; g_grant_fmt = AUDIO_FMT_S16; + g_sid = 1; g_has_stream = 1; g_dead = 0; + pthread_mutex_unlock(&g_mu); + return 1; +#else + pthread_mutex_unlock(&g_mu); + return MEOW_ERR_NODEV; +#endif + } + + int fd = conn_locked(); + if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; } /* audiod 不在 */ + + m_audio_open_t o; + memset(&o, 0, sizeof o); + snprintf(o.app_id, sizeof o.app_id, "%s", getenv("MEOW_LOG_APP") ? getenv("MEOW_LOG_APP") : "app"); + o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; + uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1); /* 单调递增(单个挂起) */ + g_pending = req; + g_resp_len = 0; + if (send_frame(g_fd, MSG_AUDIO_OPEN, req, &o, sizeof o) != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; } + /* 等响应(3s 超时) */ + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3; + while (g_pending == req) { + int r = pthread_cond_timedwait(&g_cond, &g_mu, &ts); + if (r != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; } + } + if (g_resp_type != MSG_AUDIO_OPEN_ACK || g_resp_len < sizeof(m_audio_open_ack_t)) { + pthread_mutex_unlock(&g_mu); + return MEOW_ERR_IO; + } + m_audio_open_ack_t a; + memcpy(&a, g_resp, sizeof a); + if (a.err != MEOW_OK) { pthread_mutex_unlock(&g_mu); return (int)a.err; } + g_sid = a.stream_id; + g_grant_rate = (int)a.grant_rate; g_grant_ch = (int)a.grant_ch; g_grant_fmt = (int)a.grant_fmt; + g_has_stream = 1; g_dead = 0; + pthread_mutex_unlock(&g_mu); + return (int)g_sid; +} + +int meow_audio_write(const void *pcm, int frames) +{ + if (!pcm || frames <= 0) return 0; + pthread_mutex_lock(&g_mu); + if (g_direct) { + size_t nb = (size_t)frames * (size_t)g_grant_ch * (size_t)fmt_bps(g_grant_fmt); + const uint8_t *p = (const uint8_t *)pcm; + size_t off = 0; + while (off < nb) { + ssize_t w = write(g_dir_fd, p + off, nb - off); + if (w < 0) { if (errno == EINTR) continue; pthread_mutex_unlock(&g_mu); return -1; } + off += (size_t)w; + } + pthread_mutex_unlock(&g_mu); + return frames; + } + if (!g_has_stream || g_dead || g_fd < 0) { pthread_mutex_unlock(&g_mu); return -1; } + /* 归一:native → grant */ + int ownfs = g_own_ch * fmt_bps(g_own_fmt); + int gfs = g_grant_ch * fmt_bps(g_grant_fmt); + static uint8_t out[MEOW_IPC_MAX]; + size_t ofs = 0; + int done_frames = 0; + const uint8_t *np = (const uint8_t *)pcm; + for (int i = 0; i < frames; i++) { + /* 取 native 帧样本 */ + int32_t s[2]; + for (int c = 0; c < g_own_ch && c < 2; c++) + s[c] = sample_from_native(np + (size_t)i * ownfs + (size_t)c * fmt_bps(g_own_fmt), g_own_fmt); + /* 整帧才能进 payload(每帧不得跨两个 WRITE 分片) */ + if (ofs + (size_t)gfs > sizeof out) { + if (send_write_chunk(out, (uint32_t)ofs) != 0) { pthread_mutex_unlock(&g_mu); return -1; } + ofs = 0; + } + /* 生成 grant 帧:own_ch==1 → 复制;own_ch==2 → 直通 */ + for (int c = 0; c < g_grant_ch && c < 2; c++) { + int32_t v = (g_own_ch == 1) ? s[0] : s[c]; + v = convert_sample(v, g_own_fmt, g_grant_fmt); + sample_to_grant(out + ofs, g_grant_fmt, v); + ofs += (size_t)fmt_bps(g_grant_fmt); + } + done_frames++; + } + if (ofs && send_write_chunk(out, (uint32_t)ofs) != 0) { pthread_mutex_unlock(&g_mu); return -1; } + pthread_mutex_unlock(&g_mu); + return done_frames; +} + +int meow_audio_pause(int sid, int paused) +{ + pthread_mutex_lock(&g_mu); + if (g_direct) { pthread_mutex_unlock(&g_mu); return 0; } + if (!g_has_stream || g_sid != (uint32_t)sid) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; } + m_audio_stream_t s; s.stream_id = (uint32_t)sid; + uint32_t req = g_pending + 1; g_pending = req; + if (send_frame(g_fd, paused ? MSG_AUDIO_PAUSE : MSG_AUDIO_RESUME, req, &s, sizeof s) != 0) { + mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; + } + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3; + while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; } } + pthread_mutex_unlock(&g_mu); + return 0; +} + +int meow_audio_close(int sid) +{ + pthread_mutex_lock(&g_mu); + if (g_direct) { g_has_stream = 0; if (g_dir_fd >= 0) close(g_dir_fd); g_dir_fd = -1; pthread_mutex_unlock(&g_mu); return 0; } + if (g_has_stream && g_sid == (uint32_t)sid) { + m_audio_stream_t s; s.stream_id = (uint32_t)sid; + uint32_t req = g_pending + 1; g_pending = req; + if (send_frame(g_fd, MSG_AUDIO_STOP, req, &s, sizeof s) == 0) { + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3; + while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) break; } + } + g_has_stream = 0; + g_sid = 0; + g_dead = 0; + } + pthread_mutex_unlock(&g_mu); + return 0; +} + +/* 从 audiod STATE 文本解析关键字段 */ +static int state_text_parse(const char *t, meow_audio_state *o) +{ + memset(o, 0, sizeof *o); + o->route_port = -1; + const char *p; + if ((p = strstr(t, "active="))) o->active = atoi(p + 7); + if ((p = strstr(t, "app="))) { + /* app_id 只取到下一个空白(状态行是空格分隔 k=v,整体拷贝会吞掉后续字段) */ + p += 4; + size_t k = 0; + while (p[k] && p[k] != ' ' && p[k] != '\n' && p[k] != '\t' && k < sizeof o->app_id - 1) { + o->app_id[k] = p[k]; k++; + } + o->app_id[k] = 0; + } + if ((p = strstr(t, "sid="))) o->stream_id = (uint32_t)atoi(p + 4); + if ((p = strstr(t, "rate="))) o->rate = (uint32_t)atoi(p + 5); + if ((p = strstr(t, "ch="))) o->ch = (uint16_t)atoi(p + 3); + if ((p = strstr(t, "fmt="))) { + int f = 0; + if (strncmp(p + 4, "S16", 3) == 0) f = AUDIO_FMT_S16; + else if (strncmp(p + 4, "S24", 3) == 0) f = AUDIO_FMT_S24; + else if (strncmp(p + 4, "S32", 3) == 0) f = AUDIO_FMT_S32; + o->fmt = (uint16_t)f; + } + if ((p = strstr(t, "written="))) o->written_frames = (uint64_t)strtoull(p + 8, NULL, 10); + if ((p = strstr(t, "played="))) o->played_frames = (uint64_t)strtoull(p + 7, NULL, 10); + if ((p = strstr(t, "underrun="))) o->underrun = (uint32_t)atoi(p + 9); + if ((p = strstr(t, "paused="))) o->paused = atoi(p + 7); + if ((p = strstr(t, "port="))) o->route_port = atoi(p + 5); + if ((p = strstr(t, "vol="))) o->vol = atoi(p + 4); + if (o->app_id[0] == '\0') snprintf(o->app_id, sizeof o->app_id, "-"); + return 0; +} + +int meow_audio_query(meow_audio_state *out) +{ + if (!out) return MEOW_ERR_INVAL; + pthread_mutex_lock(&g_mu); + if (g_direct) { + memset(out, 0, sizeof *out); + out->active = g_has_stream; + out->stream_id = g_sid; + out->rate = (uint32_t)g_grant_rate; + out->ch = (uint16_t)g_grant_ch; + out->fmt = (uint16_t)g_grant_fmt; + out->route_port = -1; + pthread_mutex_unlock(&g_mu); + return 0; + } + int fd = conn_locked(); + if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; } + uint32_t req = g_pending + 1; g_pending = req; + if (send_frame(g_fd, MSG_AUDIO_QUERY, req, NULL, 0) != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; } + struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3; + while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; } } + if (g_resp_type != MSG_AUDIO_STATE) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; } + g_resp[g_resp_len < MEOW_IPC_MAX - 1 ? g_resp_len : MEOW_IPC_MAX - 1] = 0; + state_text_parse((const char *)g_resp, out); + pthread_mutex_unlock(&g_mu); + return 0; +} + +void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) +{ + pthread_mutex_lock(&g_mu); + g_preempt_cb = cb; + g_preempt_ctx = ctx; + int need = !g_thr_on && !g_direct; + if (need) { g_quit = 0; g_thr_on = 1; } + pthread_mutex_unlock(&g_mu); + if (need) { pthread_create(&g_thr, NULL, audio_thread_main, NULL); pthread_detach(g_thr); } +} + +/* 读线程主体:poll 读 audiod 下发帧 → 分发 PREEMPTED / 挂起响应。 + * **不**自行 connect——连接由调用方(open/query 经 conn_locked)建立并启动本线程; + * 这里只等 g_fd 就绪后 poll(避免与调用方 conn_locked 竞争出新 socket)。 */ +static void *audio_thread_main(void *arg) +{ + (void)arg; + while (!g_quit) { + pthread_mutex_lock(&g_mu); + int fd = g_fd; + pthread_mutex_unlock(&g_mu); + if (fd < 0) { usleep(300000); continue; } + + struct pollfd p; + p.fd = fd; p.events = POLLIN; p.revents = 0; + int pr = poll(&p, 1, 200); + if (pr < 0 && errno != EINTR) { + pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); + continue; + } + if (pr > 0 && (p.revents & (POLLHUP | POLLERR | POLLNVAL))) { + pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); + continue; + } + if (pr > 0 && (p.revents & POLLIN)) { + uint8_t rbuf[MEOW_IPC_MAX]; + uint16_t type; + int n = read_frame(fd, &type, rbuf, sizeof rbuf); + if (n < 0) { pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); continue; } + if (type == MSG_AUDIO_PREEMPTED && n >= (int)sizeof(m_audio_preempted_t)) { + m_audio_preempted_t pm; + memcpy(&pm, rbuf, sizeof pm); + meow_audio_preempt_fn cb; + void *ctx; + pthread_mutex_lock(&g_mu); + cb = g_preempt_cb; ctx = g_preempt_ctx; + if (pm.stream_id == g_sid) { g_has_stream = 0; g_dead = 1; g_sid = 0; } + pthread_mutex_unlock(&g_mu); + if (cb) cb(pm.by_app, ctx); + } else if (type == MSG_AUDIO_OPEN_ACK || type == MSG_ACK || type == MSG_AUDIO_STATE) { + pthread_mutex_lock(&g_mu); + if (g_pending) { + g_resp_type = type; + if ((uint32_t)n > MEOW_IPC_MAX) n = (int)MEOW_IPC_MAX; + memcpy(g_resp, rbuf, (size_t)n); + g_resp_len = (uint32_t)n; + g_pending = 0; + pthread_cond_signal(&g_cond); + } + pthread_mutex_unlock(&g_mu); + } + /* 其它类型丢弃 */ + } + } + pthread_mutex_lock(&g_mu); + mark_dead_locked(); + pthread_mutex_unlock(&g_mu); + return NULL; +} diff --git a/test_ui_audio_host b/test_ui_audio_host new file mode 100755 index 0000000000000000000000000000000000000000..3890d78820dfc1a92c07b10769cd9402f7221555 GIT binary patch literal 74112 zcmeHudtj8+weOmlyo7)u5I~@00%#$X@O~-C!W4v?U9I{)>~~G@r7-*lc3eAJ>@Bh1?K$L_w7CTX2^Kj z`}l8vIN5vs_S$Q&{o89lzK3lq)~+<0Oe8N0{e)Pujn@&ID6mZ!)UnZG8b&ty3QeLc zlxN_V(3AQioo&`LGR+aV1u55S7v-txM4=~B**d40qHL(du}DW{N?LF9zwGPmi+cI& zi@HUbe#Q>r>aWmKS&!Z0dNxtdChEyFPxvoW*+1?Z|NNpnHJyeszGTX}d~wP@o9IVs zYDF1eR_(8@%h?SdhHABXbM*pSO8T16m#J*;Hq_(({k2z~P^=W~rQ@?l@F`O{e%$UD z{J414t+&(KpM3k_*vqy@uG{fN^9#@a>&HvJ{hM>M8ay?#8fs@Yc)ZOmGo8+Wt8U@^ zlAy0-jy@`EOWH&Buf8mZ+ODgwRJYJrfMTFT@z2me{*|h9OB#G*8oV(L4w$S%*bf04 zs@rLpfxqkmf({a>chFG!>RNgDi_G{r;bLYWuAmnRqXdr)cD98r9Y{At4J^N;N zDCB8mAJ9BxS6#h*lhe}xK)1*UTXlQ4P_q|nbv^lgu*I#oL$t{ks3-P>nu85)w;%Ny z-HouX*9!&ET*6tG&s%Hf=4!V(J)vYc=-KRb!bQ<97i@;y39+#`McdIb@b}C9Woo5-#?iI7 z7XJqS@y?k1k6SU}-*{}9d>*jkw?DEE$A~N{5!)!2XIxR9X(+eQNKuaJbVRG4K}!Vo zoF-eSLdd#(bE3XHw^_tN#&Hc>AzCcXXA;j9_^CK1ATEh_i^-dVz8P^T_XvFx>}P8l z`kxa9e3$``8E|=K<>HSG_#}gVzX6|N!0RsSewxv)I6`TqegiJLtl=#N+;}by8*mIv z;=12}%d;vM?l$08CA5hS7;s~M4;k>`2K~bZJlBAC8SoJX{D=V`X~4S;xbevLiUF5r zQLffwz>WLs2?K6iuVV&$w4vRP4LF}u<#oz{7f29kzX9h{Wa1Lz%i}mM0TSgHaB+*J zmF5|65waRS-hfXq*s&ULW4ui_;Cu#=*K7kmS%OFx8}LE{ZZqIk1HRgT7a8yk27HPE zzr%n}HQ){dKFxsF8SrZjxZi+ZXTVzw_*V^h*nm$r;P)HwVgtV0fPc+^A28s229ehx z13puNNDmwE5(D04z-JloBL;l70q-{8xa~NE&0>R7NYD)y$o{Cs2d<3GsrhV?lZ_`kj*z} zwlw{fkf#{rn<1AOK7(8X*?i+*`bZ*tarEU2gm+sAooUIz&fwjA<{qdV3=Qob&!1~_xIp)L# zS3yVci<=??&(Z+a*sjkOjcfe(<>PUSZX4+R9u1f%eB8SHrpRyO@%=H&n9g^R7D1ju z;mVk0bmt1l)%fA;5bd_m0>-ghcfpmeqQ%Q2w03!9Daw0Mm&+KZ2bM<`rP!GVIjQa| zsN1tVG7~uL^%QNOh#9`1eh(g&L<)h&t`1igAf@dORpOrNRG1D0_Oi@6?GyTyMM&4nrnH9x+RWCb9hc+Py_V6%G5A=N!G2hqw7zaPMPlO)PG~{< zv8fcfFO%BC6KFwfxGh?kK}}qy>Ah@<_@HynWr}=QXd5^=k0O20;j$0r*`h`Lricyw z$b%0>NMn|Q;$Lg+klxM_v)0ZxMLP{>C$?U%U({oc$hPqOq8%J+^^c1B2_G^nky}yD z_Ax%tF%?fkduPF?&D_6e=ZNret!}+3a^`Y;|49pVTA{}=6nmChV-`#2BN2ha4nEa0L0Ol&y~SYzKgZZ?OH$2vOBSb@>_*qN9QxG8)*oC%#E4RE{f z;xVi>gBD1?c4mf;zX>di81J2GiLega`2<+Z4D3}~6t*j2H?W+jzSK>MIma4^f#(VRr}>1wb9`!j z^g|c%c@DAu@Iw^AzSB;JcT_qM_wRtG@O5JBeridK-AB-wGMXY>pJSnjG9nK^*F-xi zIqvR7{L}WrD$V}djL2`fKZvxT_(+@ag5oE49c zGv|aWA4eMdtu6Wkq*k=|4$^^VZCE=iJI+>Bb)4T=-EqNnd&g&szK*r=jz+An5k6bU z^FwNCpJsXMD`;ynejg&+=IUuGY=rzlJj^z~E`QpDS*-G~{j?;o!kaKgqZi>Z@v&S5*L`b9*qPPy3jMrrPj zT}2m5_q+)GKD1wUan(SLR@a=UtDmi2(9Tw8htt4WwCUJsTLK;z!EO(IwrU`ZI$9eU zwt;^`e@_6W8!R8DaNR*FpqWM8RzPoI&RrJBK$RYUSSZRw= z290v8q^RUn>#I>W@21FJ*2O%~bZ@5Ud9Cj$<2Tk8#rnGF5$F>9cZ{Vd9!+$y?`+%T zrznpZ+y7g}=xNxCQU8yBs@c`Y=#UsA+;5NRj>n&qe6AK_lgj7c(q2Hc$NUiepS2e+ zH@$von;B;$j6szJacJp0Ysn}+iP&~w9XUUh2Ka1Hi#6nft7(Ae>{P7tC$Y|-98S@x z;Oyi?YV9q=4|BY4y?F-j3%}wyfPG;!#uMxHbA4Fj^}VE^_~$6o)+fusJGD8Bc1~WM zyiSF|TZz`MaQ+31;rXdNkIJwfm3P2rZT;c+(PBrNKU5rl>8jTegW9=IJHIV5Y3DZr zRti69X;$RYMZ`x||045ReP8B%n%Dc`Xp3XG7eB0%`C+WXQ*ulkM@>buDe?s3zjseY zgm`_vnh-xoXHnG3>3WLBEQQZli3WO~tymH}S+S(3VN!m)&D2puljjohz0YPWA>@mu z+<)A(_Q;v|Xc~xrjiT{3OGg|W$8X_$W=DJ;^7RxwH_sG3x5yNYoy=Im`#|4tbF>d* zQwl$MjxhEK`od=e+2_7RrRcA1po~nP@}2>_`aS3*_L1rLAD@Hz=fEep()L}BKN;(B zizUI|rENLj)*QL8kp?bcUZ1VHs^cu?&H0VP@Y(=pW1JdNEeL3ShU}l72&xi&r2tur;UTDe-ZP^aZ-bGQ*1QO zM_;2z{2kMhlhEP#;=b@1=_SZvlYT}zjC>Lg%Q1%yJUA>={U~&vhmQ+k|0K=>y;xJ( z*0&%}!I*xYP0;{!&SEV%G|U#ooLX?Gj3T^GaX+zs?Jn!ODe~iC6g@hUqCbQ#^adV7 z+6O-8@i-v8u+cWab}wyPjaXfSb@}EF@T$e@2OMkQlE#21obTkC!|M&9zP{cZ?XZAP z=7wY2wL2qHIzC)E7IOyt&HtsocjxmxzOMhjIz|lJd0fr|-ajt5EFA+JQ?8k>#x3Rn zw5iPr%*C!&aG;IfS97%SvtA8f5KmSYMfNdAu+RDZ$fI5t-$V6%b#)S-v5{203gxW# zUv<5n@#vG3k8s~`PZGwttO?~GffpWE^EG__3Lo#cm}c;tn~X9C#?X&cu3zE5#H-m( z5ZFV)mk{!A!^WX6;`3vL-K~W=UkUVL{j`3Oy|DKr$HaK;EV>u^@Ogd+eGJ!7qz3hH zkFlTge@6Y@8Q>4!_25Z+i zoX^J>Z-+fXZ2W$LrRhb))xig;>5u2*)tB$FJ;m+Drr4sfc~~FN=H)m&RSF-%cj8<& z+7{jSenkh)D)l=sFPRTVfT9QQ89u{-dgP$U4ba_pHvXK}Rux6M&CB0D0Kdo3&dT?Z za+~2Z@#^2>jQsM1vbzxv)xV!&Y3e;4uinaSqs?n!BUWRJIxwc1-|(N~j1Whk|AJb- z{TGx&H(rA^<}Ql92wAqz=Rb}m#0_4#g^v$l&6WGp1y?!NiGR~#E?#cNo{`yc3G49L zsx0goT3l8J zvCXfP-Tl8I^LjM~HktR|q0Xn!cVyY3Tkjk*BW&jNbxd&`#v1kKJOO=fd%h{0|2nr1 zUEa5jV*IS*DQZJ|A#=eDO^5T9!)Jt^q3F+``xbONVds!`KL;B;&w>9MIBc))N34%V ze;f<5BZY1BRN+fImUf}snnBMo_7w6bv4*m3UV}^-;p2RUx)J!f8?jcQ9~ZV!>*sCM zI>up(9>jg(A>4bl<31cb^)RO=QI}(Z*PU-=;(UhnB!uyA!}t??<5>9#_5}yHZ-;*e zVEc#g&jP<(4ojpLdHAhy%4={Q%46BKfvwPQfqlxw9L%Nx2jZ@f#?;$zZgl*D=ar?2 zW8HBV@TJuB3UKz9IsXG}_Q95zSDpU|oA6~mpWnuS=NRVO7GU3oo{V2}hOL8roO;o* z2j`Sx%lY_%nmZ|4cG2+$k3~%XFaYh9hY9B$OTsx;+HzHR`Hsx#rD~%jE=aOPM_bH*^x2P zcK1MicE?|WIr@G$PLJ^s1ynr-vAhTUWM50sHpf*sljb{~4o6LGwmqL?T;8svNCD;; zZ8tsYe8v{tgM3jA-L^cJno5xm&fW1y@E&^fD9-W!4ZNMl+7lMs&4x|sQ{TAcIna$c z5c>kpqtIrJx!~1abHT2UA@jY%Sk#m2tnE^~n(e^{Z4OLC8P9>|aqn;vDbG8KV>|-3 z=v$ZL3pkcIejJzMiyS||S_W?6XC)rQ>?ZhqC(cHN@ZIsGE&A@<9TO&^4@=r;q62;7 za;@xXTl8&|b>Bl1$D%xj^CRo?S#TX<)(N}IF<%|S@VwxzEk_U^+VdX7XSEeJ%20>z zqr*sP_s&O%#_nQ$>s9FUU5+EMuVUT6^B>1c)?I}ew$mNjyq#D)h38*?d;$9uyO$!9 zIJF>N;ZGq=s&9ne!Qpfo@Aa#n$Gmy_*pgj^mV)~0u@^nc_jZ@Vx~%syq$7zPmned7A6q6>B+y6ysglgj8!2{li=t{Ts-awv}UE6kuLd zU|y`iyja;WP`?WCZI~bT4&E!A-u0TlYEswY4Aj3?}%=bKxHN z=|Ef_!g?cl{0CqJpRk^G@pFsR`vA(K>g%~4;xUZ9-GO;rNCoxX;8faZK)Drh%zHe> zH42}4nz0sPt~PPZ_Tm{0k84q`Wfay@?B@mAxvv>(U^n(l*qXl;Df{>h*aCO?zk@Aa z>)smB*Sh32uAtuZMZB8VFDL3a(ATfC5121(V>R-)Pso2&<5jcud zAWhtN4aFtSY8sb(rq~CYMlSI@pz~|&Cv%Ct>LAuQ+++3N4o5qK+i-7t5`MR{jJ|}i zpR$j^qBjXC959KijQk-O28>AA7s_D!?{e{ps!=sdxF2AAF767zPw zBcCE|W#OfT7wIYHVn-+DwHaqR@Zq>GV@6xqy-UMbI~;dXYZ!Cg@l9%Fp4;-n%N#fJ z*`=WK=dj6s^L>9OIMMG_3OZi|ZiQ~Tc(+|c)qds;qeSe!!utW*?BssH_8&#;UdMK@Pd|JG|Ks%e!+W$9XG*L+VQ_H@_4F|*z?fXq z`6Syz9I)*@$a72|ht3Y{=Qyu*^~HBo9(oAtJ~&%`ad}hG?wu0~>x%F>mCl6|T$p%X z*JVfA?xCmJ?lC>((R{?&ZxTiFk!N06hwT*IpB-@m!~OO13GDmXk^ds=;Q3J|*SXG6 zrz<;BEz9v<U?yF2}ef?4QVvRG>VD^XM4ZXPlp@7k!o;sR8e7 z5Bt>o1b$J+FR&fptjl^_UX+^?ISrimGbhHacl3@4IDZVbH7O@D18rfwd5)}j7KZyy zUK_jE)&!1I!GkR)QYiJnlhpeG>rFQ3IdUQevTxHU@&cEa8Tx*IPGp2Ehn*jD`AS3i z;T(<6F#7}Up5}5fX4-gk=V*Lt<={qZ|5l1R@@-2F;a;Bo<+)PyW=`Z&)a~Xt!x~eH z7}<$9D9XTdzdGmQ@|FqtC$YwsPovvhlPJFp`<(~#fBAMg&G%qVvuW4zJX&%u>hUwe zTG(V>c>gK_kN<+S6zNl3j@X)mbYR}#^Pt4K@REEkH27Rqp2hgSn4hWQ*=i@R(Z7Yi z&YiXyTi(vVF~L-fI9#v*Z8Q(Vxfu8N?dS{7iMOEl7VlSOgUS+jkt8)vjTe1G@_R`xc)~vTztuMcYHhJ*Xpb=bi z>n%ok{q5B>#|W%kUs=9_N=giE)*AEnI$tnE%a$#(YECtKbib@s6}MQq$Qp9v56ZRg z?yS=_uWPOTfIH~+hGys=z**{kguLw9 zTC3CVUv_QK8UVcP+GZ<%L5ENCmR;M>fZ_5xoAF7WZbHk~;uFJQGd@!cIJa8mCwj=w zgc%Lv&jGE|;g7q~4}b7ArN~=1c>?ZQcpzkVt+l3A8w2g5J*#sQzQ&Y=)|oe1m(I3M zuf_L_GerBr5I!8VZpCMgLH$!fE0_*PJJEg^Dcd>7~oXgpTkzHVJ<AWBKfg*?yYU91P5=b%vaVVbNp4FwPZgDp!jS%X z{VIFuh7F`syo{Jof>+DH2RJ2H-7?zL7+OYEa~4w7-1$^BZ>}~@Of&f#oULx?Hhb|+ zYM_}B<{XG0A2BYYExv|jG?VDXeRo})_wYa4OcO0P^8Zu7-!g@9v)qq`f^ft40TKt& zex$pR<^q2WsTHXehuYOhrz3SBZAZEr={cm`jDH-Dzln4=($%;Tehp~@(xuq!4j}yz z(wC9eeS*n}^ayTdCRyOapW^WiNE?v)k;ZUH{1H+-fsNyN9G$=mf@vA}e=hKl`3|IY zcv2s(E_$B+D#eY;&!q3ON`-S7XEx*WGL=>Al&_m0j7H!ummZ=p^%V!Rr8x_M;YW9G+AH^2X$*8XBZ|Ag{$C=Ve; z`x_n`_V`t+Mjk5KlhdBPH)~(!{)_{bht1{(M{Zd8{(FLLuFT^bL!ImK!0d!xceklj z^M{?OU}xaj0h9T^%OTR*B}&2ozs8WCEvtCQ%&~76=GclvjIk;= z!*hsTuvhReEtq8eE<^nx*>Tr@}HguGiAmiQ6TuVm|z5BiWrlv_=l;k&O6BT&v2< zZ?I0ky{6e4YPQZBgP41vI0=~Vo2x}7y1`zE(D)Esczo!(k|jVBZ& zo+Tz>d+3&lEYR@FiDXc+pkDisxFs>a4eGFyL2_Ov|TrxhKBaMfn&rG=_ zak;PP{$CkaI(}uG%>2@Jhf%j!m!YOz(oStkT+K)6m-%0pG6ZX&&;CoiK{rq91^%Yc zcL@9(28{LRiw&OJfa~uh7??HH;)db#OHUR z|7GKHC?&2AK%eU43-!JglqGM+Sek4UdF7WOY zd<43~e7+)Z>E~#Hzm|ft$of4gc(K6W6gWifS}5=nDf*=Xj|p7*zgpmVVtl1Pw+Vbq z3cgX`<5TcDflo@og95jv;BwyGFBfv*f4cQb>Mu<>hqP(-^C%T(b3@J3?`ry|Bx8lQvrBXYU?a~k~%Y3vWfgRPAjAMHm{T~9iYCTE*^I{tP_tM~h zPJ@rYgSve4mG@br8BcCfljfztSEj-5NQ2*<2B#`lAT+1MN7GtpS}ob_{In(H+)RKy zfL+dp2K!dGv);bR%a|8f$Q=yXn>}`CbFIf`uk!^%B`(@(hb9HIzpgsBtDgL>#**2z zSy!vI`#rS~`~kPS(I0YizNVD{XE2Cllhf0{_Jg73O`Bk@)*Wzf_5?%jfIZY`cQyFD z?jXFY_1QN!_-dRD_S%py5QOa(a`_tl4Q`l3YeQ<-H+j79HV|;O+TGqzpp`ZSoQ-aK zZF6H|E2;5_$zYrqr26Ds6_Zak*a ztWvEf;C8`cL(pmW`Q6~q=k>~}I)R}&%|xTqc-ue=#2SfgV&yUXennN0) z0e8^Plre{}FEVgLm(d?ff$ZTrlHa7>H-jF@u3~>!z&@}Wc>)7op zD@)g{u&=na!fq#f#W!v(UALwj@TyyHx35?&N>^7@l6}?M^<|}N?dw;rtXffRuP!ZH zyTVS5?nalt)wXKwnzC~H+>$wq$nI|OgpvSzgQv!Y{x}06JCmF!oHw7`EpAss0^sUv zs@Gw7W$;z6E9cOwcGfhw;gu)obIqN{+=P714TBZuEk=?(8+q)&nu-(<- zkK);4B}Q; z<+uxa{Q-|Rv?)o|uJea+OE$q1M)Zh7*-p-o&tt5K{(*{mpFfFe4sfHE-tyurmz4L4>vx z84YO-8v(n0b0bDdjnom}+7f(BdFvR`aw>Gb!DHfY--Msx##A zCB+|RPs%Km^fWey+%4KNY;;Ro{S%$nb(+0A5sg|~ot_XjGb02gk2gt4Hl&3*PvKgw zW~}4!xk5?dPbI}@WoY4$YK&crHRg30U-_(?;tVAoMe zZL1d+^fVOEOSZTJL9mh}*ija6H#oV1$oU&WY!N#}35da8Z3QR^__Vd8#9b#g$+}w9 zl@gU`Zk3j;naL*xt-ii@>h)zF%{Uu9F8JVsW!)lTjj*{jHey4jlG4g`CC*SN;Hkm5 z1cx{V{ml?R?!u8Laq5%rfn+L<*9p7wTrL5=EyWQxQD43&wTis|7CW%lA|=mG7mY zrd`}uDDL*9YOE=~FK$*%o zl=_lCDf4rGS>_{Z72EoPANRk=mY=Wy<~IS75Tfe6P9qudL7U&9Nuz%jb?ZQ9n-%jJWMi#IKB_ zUesm&WqtXcbi-wUxVpH|a7n*q`UjL5>krOEy<$rECEAxJKSPFX$@Z;cCQs6=qO4SE z>Zxp3qNkDJmgMthiOc%eNkF8!S9%?WzQ7M{`$cggsSxc?OCWkCb^O_$jD4eOov3dU w50iKw;0;UqFXt6M=aqdo);~N-_jDHrr{R+RONh-6Re# +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TSOCK "/tmp/meow_audio_ui.sock" +#define TWAV "/tmp/meow_audio_ui.wav" + +static int g_pass = 0, g_fail = 0; +static void check(int c, const char *n) { if (c) { printf(" PASS %s\n", n); g_pass++; } else { printf(" FAIL %s\n", n); g_fail++; } } + +static int g_preempted = 0; +static char g_by[64]; +static void preempt_cb(const char *by_app, void *ctx) +{ + (void)ctx; + g_preempted = 1; + snprintf(g_by, sizeof g_by, "%s", by_app ? by_app : "?"); +} + +/* 小工具:raw 客户端连接 */ +static int raw_connect(void) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + snprintf(a.sun_path, sizeof a.sun_path, "%s", TSOCK); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + return fd; +} +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; +} +static int raw_open(int fd, const char *app, int rate) +{ + m_audio_open_t o; memset(&o, 0, sizeof o); + snprintf(o.app_id, sizeof o.app_id, "%s", app); + o.rate = (uint32_t)rate; o.ch = 2; o.fmt = AUDIO_FMT_S16; + if (sf(fd, MSG_AUDIO_OPEN, 9, &o, sizeof o)) return -1; + uint8_t rbuf[64]; uint16_t rt; + if (rf(fd, &rt, rbuf, sizeof rbuf) < 0 || rt != MSG_AUDIO_OPEN_ACK) return -1; + m_audio_open_ack_t a; memcpy(&a, rbuf, sizeof a); + return (a.err == MEOW_OK) ? (int)a.stream_id : -1; +} + +static int g_audiod_pid = -1; +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; } } + +int main(void) +{ + unlink(TSOCK); unlink(TWAV); + setenv("MEOW_AUDIO_SOCK", TSOCK, 1); /* SDK 客户端连接测试 audiod 的 socket */ + g_audiod_pid = fork(); + if (g_audiod_pid == 0) { + setenv("MEOW_AUDIO_SINK", "file", 1); + setenv("MEOW_AUDIO_OUT", TWAV, 1); + setenv("MEOW_AUDIO_SOCK", TSOCK, 1); + setenv("MEOW_AUDIO_FORCE", "1", 1); + execl("../meow_audiod/meow_audiod_host", "meow_audiod_host", (char *)0); /* SDK 测试运行于 sdk/ 目录 */ + _exit(127); + } + usleep(300000); + printf("=== meow_ui_audio SDK host test ===\n"); + + /* 注册回调(读线程懒启动) */ + meow_audio_on_preempt(preempt_cb, NULL); + /* 无 audiod 场景:断开后 open 应返回 -ENOENT(先测一次) */ + int sid = meow_audio_open(44100, 2, AUDIO_FMT_S16); + check(sid > 0, "open (audiod present)"); + + /* 写正弦(stereo:L/R 同值交织) */ + static int16_t pcm[4096]; + for (int i = 0; i < 2048; i++) { + int16_t s = (int16_t)(sin(2.0 * M_PI * 440 * i / 44100) * 0.3 * 32767); + pcm[i * 2] = s; pcm[i * 2 + 1] = s; + } + int wr = meow_audio_write(pcm, 2048); + check(wr == 2048, "write 2048 frames"); + + meow_audio_state st; + check(meow_audio_query(&st) == 0 && st.active == 1, "query active"); + printf(" state: active=%d app=%s rate=%u written=%llu\n", st.active, st.app_id, (unsigned)st.rate, (unsigned long long)st.written_frames); + + check(meow_audio_pause(sid, 1) == 0, "pause"); + meow_audio_query(&st); + check(st.paused == 1, "query paused"); + check(meow_audio_pause(sid, 0) == 0, "resume"); + + /* 第二客户端(raw)抢先 → 本 SDK 收 PREEMPTED,write 返回负值 */ + int rfd = raw_connect(); + check(rfd >= 0, "raw connect"); + int rsid = raw_open(rfd, "raw-app", 96000); + check(rsid > 0, "raw open (preempt)"); + usleep(200000); + check(g_preempted == 1, "on_preempt fired"); + printf(" preempted by %s\n", g_by); + check(meow_audio_write(pcm, 2048) < 0, "write after preempt -> <0 (dead)"); + check(meow_audio_query(&st) == 0, "query still works"); + + /* close 旧 sid(已死,应无副作用) */ + check(meow_audio_close(sid) == 0, "close dead sid"); + check(meow_audio_pause(sid, 1) < 0 || meow_audio_pause(sid, 1) == -MEOW_ERR_NOENT, "pause dead sid -> error"); + close(rfd); + + stop_audiod(); + printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail); + return g_fail ? 1 : 0; +}