diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index 87a503d..0185510 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -1170,3 +1170,126 @@ ed2f71c52563e812a04fb8a7cc05148d app/test/test_ncm_app.c # settings mock 初 | 设置页行数 | 4 | 5 | 新增「清理缓存」行并在「缓存上限」接「自定义」数字输入 | | 音质 sheet 档数 | 3(Hi-Res/Higher/Standard) | 5(+Lossless/Exhigh) | 对齐 core `NCM_LEVEL_*` 全档 + 降级链起点 | | `ncm_cache_clear` | — | 新增 | 删除 `song_*` 全部(mp3/封面/.tmp),供「清理缓存」与测试 | + +--- + +## 16. A2:音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)+ 无声根因修复(2026-09-01,agent-248) + +> 范围:仅 `patch_v2/ncm_app/`(含 build 脚本、bench、REPORT 本文件)。未动 `sdk/`、`meow_audiod/`、 +> `meow_launcherd/`、`vendor/`。风控纪律:真实 NCM 请求 = autoplay 验证的 1 detail + 1 song_detail +> (Docu 单首歌单,共 2 次 metadata,**0 次 song_url 下载**,曲目走已缓存 mp3)。 + +### 16.1 迁移内容 + +- **`app/play/audio_out.c` 新增 SINK_AUDIOD 后端并设为默认**(AUDIOD_DESIGN.md §7 落地): + - 后端选择(env 驱动):缺省 → AUDIOD(SDK `meow_audio_*` 连 `/usr/data/meow_audio.sock`); + `NCM_AUDIO_SINK=file/null/alsa` → FILE(WAV 回归)/NULL(解码 RT)/ALSA 直开; + `MEOW_AUDIO_DIRECT=1` → ALSA 直开(调试保底,非正式能力)。audiod 不在 → open 返回 -1 + (不隐式回退直开),日志记 `audiod open failed rc=...`。 + - **ALSA 保底路径修复两大坑**(与 audiod/SDK 同款修法,注释标明勿回退):① hw_params 改 + 「全放行」初始化(mask 全 1 + interval [0,UINT_MAX])——旧 memset 0 的 interval 等价 + 「只能取 0」→ 内核 refine 必空 → HW_PARAMS 恒 EINVAL,这正是**网易云「播放无声」的根因** + (真机 probe 实证);② access/format/subformat mask 先清零再单点(留全 1 会被 choose 成 + MMAP_INTERLEAVED → write() EINVAL)。 +- **被抢占处理**(`player_manager.c`):`player_manager_start` 注册 `meow_audio_on_preempt` + → 回调记 WARN 日志并经 `player_manager_pause(1)`(g_play 命令通道,持锁)暂停播放+更新 UI 态, + 不直接动锁外状态。 +- **暂停/恢复语义对齐**:`run_song` 进 NCM_PC_PAUSED 沿 → `audio_out_pause(1)` 向 audiod 发 + MSG_AUDIO_PAUSE(停 PCM 时钟不丢流);出暂停沿 → `audio_out_pause(0)` 恢复;**恢复时若流已死** + (被抢占/断链,SDK 返回负值)→ 自动 close+reopen 重建输出流(解码位置不变)。 +- **播放路径全量日志**(meow_log,无声排查永久资产):open 结果/请求参数/audiod 授权 + grant_*(open 后 `meow_audio_query` 落 rate/ch/fmt/underrun)、write 错误(首次+每 100 次节流)、 + pause/resume/reopen/close(含累计写帧)、play start/done/switch/seek/stopped 事件。 +- **真机联调 autoplay hook**(`appstate.c` + `app.c`,非正式能力):`/tmp/ncm_autoplay` 触发文件 + 存在且 home 歌单已载 → 后台线程找「已缓存」曲目自动播放(内容=数字则歌单下标起扫 ≤3 个、 + =文本则歌单名子串单点匹配如 "Docu")。**绝不发 song_url 下载**——无缓存命中只记日志退出。 + 用途:无用户在场时验证「ncm→SDK→audiod→DAC」全链。 +- `build_ncm_host.sh`:SDK_SRC 补 `meow_ui_audio`(audio_out.c 新增 meow_audio_* 引用,宿主链接需要)。 + +### 16.2 验证记录(分层) + +| 层 | 项 | 结果 | +|---|---|---| +| 宿主 | `build_ncm_host.sh` | **自有代码 0 warning**(仅 SDK meow_ui_vol.c 既有 system() 告警) | +| 宿主 | WAV 回归:`NCM_AUDIO_SINK=file NCM_AUDIO_OUT=... test_decode MIPSTest.mp3` | **PASS**:8.21s/48k/立体声,WAV 头合法,字节数 44+393984×4 精确一致 | +| 宿主 | 离屏渲染 home/playlist/player/settings/search | 全 rc=0 无回归 | +| 宿主 | **`bench/audiod_chain_sim.c`(新增)**:起真实 `meow_audiod_host`(FILE sink 假 socket)驱动真实 audio_out.c+SDK 客户端 | **14 项全 PASS**:OPEN(grant=44100/2ch/S16)→WRITE 51200 帧→QUERY(active=1 written>0)→PAUSE(paused=1)→RESUME→**抢占**(第二客户端裸发 OPEN→on_preempt 回调触发→旧流 write 失败)→**流死自动重开**(reopen 可写)→CLOSE(active=0)→audiod WAV 落盘非空 | +| qemu | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS**(core 未动,回归) | +| mipsel | `build_ncm.sh` | **0 warning**;ncm md5 `db5b78ad9e8f40e6306484efe2a08eb3` | +| 真机 | 设备已刷 M2.90(0.7.85),/usr/bin/meow_audiod 在板运行 | ✓ | +| 真机 | ncm 热更(.new+mv),`am start` 不可用(amsd DECLARE 转发未联调,「pkg not registered」)→ 手动 nohup 起 | ✓ 启动/home 歌单 120 正常 | +| 真机 | autoplay hook:name 'Docu' → playlist[1](1 tracks)→ song[0] id=2048962657 缓存命中 → play | ✓ 日志链路正确 | +| 真机 | **audiod OPEN→grant 链**:`meow_audio tone` 拿到 `sid=1 grant=44100Hz/2ch/S16` | ✓ IPC/协商链设备上正常 | +| 真机 | **持续写帧/underrun 验收**:**被阻断**(见 §16.3 重大发现) | ✗ 未达成 | + +### 16.3 重大发现:M2.90 设备 PCM 播放启动 → 内核硬挂死 → 看门狗重启(4/4 复现) + +**现象**:设备(0.7.85,stock kernel 4.4.94+ #15 2026-01-16,未变)上一进入 PCM 播放启动序列, +数秒内整机硬挂死并重启。复现 4/4:`meow_audio tone` ×3(含一次无 ncm 的纯 audiod 路径)+ +ncm 经 SDK 链播放 ×1。kmsg 流抓取到最后内核日志(死亡点,两次抓取一致): + +``` +[ 412.104797] hbc3000_set_samplerate codec_type=4 samplerate=44100 spdif=0 ... +[ 412.104819] hbc3000_stop +[ 412.104865] hbc3000_start +[ 412.168307] cs43198_dai_prepare pcm +[ 412.371919] hbc3000_start +<<< 日志到此中断,无任何 panic/oops 文本 → 硬挂死(疑似时钟/电源域死锁),随后重启 >>> +``` + +**归因排查记录**: +- **固件打包无误**:设备 /usr/bin/meow_audiod md5 `e07e9144…` == 仓库构建 == fw_content.conf 条目。 +- **ncm 无关**:对照组 ncm.bak(A2 前二进制 19d99ce4)同设备前台 idle **31 分钟稳定**(boot G, + 期间经历 suspend/resume);本会话早段(23:05-23:45)观察到的 4 次「idle ncm 在跑数分钟后重启」 + 实为**用户 M2.90 刷机循环**(卡根 r3proii.upt 22:46 上卡,多次升级重启)同窗口巧合,对照组证伪。 +- **suspend 相关性未证伪也未证实**:设备每次 boot 后 ~40s 自动息屏进 `System suspend`(dmesg 实证), + 而 4 次致命播放全部发生在 resume 之后;A1 收尾(agent-246)同内核 tone 实测 written=played、 + underrun=0 **通过**——据信在未 suspend 的新鲜 boot 窗口。未 suspend 的播放验证需要 + 「重启后 40s 内立刻 tone」,本会话无法在不主动重启的前提下获得该窗口(留给用户/主会话)。 + +**影响与建议(留主会话/用户决策)**: +- A2 真机验收的「持续写帧+underrun=0」被此问题阻断,非 ncm 代码问题(SDK 链 OPEN→grant 已实证: + tone 拿到 `sid=1 grant=44100Hz/2ch/S16`;ncm 日志链到「缓存命中 → play」)。 +- 建议:① 新鲜 boot 40s 内(未 suspend)测 `meow_audio tone` 验证 suspend 相关性; + ② 若为真,权宜=禁用自动息屏(`display.screen_off_timeout`=0)或 resume 后重置 codec 电源/时钟; + ③ 根治需看 hbc3000/cs43198 驱动 resume 路径(stock 内核源码不在本仓库);挂死点恒在 + `cs43198_dai_prepare` 后第二次 `hbc3000_start`,无任何内核日志 = 硬挂(时钟/i2c/电源域死锁特征)。 +- 本会话为定位该问题触发过多次设备重启(均自动恢复;未碰 recovery_all/未 killall hiby_player/ + 未主动重启设备)。 + +### 16.4 md5(本批改动文件 + 产物) + +``` +db5b78ad9e8f40e6306484efe2a08eb3 ncm # mipsel 静态 App(新;已部署设备) +``` +(源文件 md5 以 git 为准;改动清单见 §16.5) + +### 16.5 出入清单(只改 `patch_v2/ncm_app/`) + +``` +修改: + app/play/audio_out.c # SINK_AUDIOD 默认后端 + ALSA 保底两坑修复 + audio_out_pause + 全量日志 + app/play/player_manager.c # on_audio_preempt 注册/回调 + 暂停沿 audiod pause/resume + 播放事件日志 + app/logic/appstate.c # autoplay 真机联调 hook(/tmp/ncm_autoplay,缓存命中才播,绝不下载) + app/app.c # app_on_tick 挂 autoplay_maybe + app/ncm_app_internal.h # audio_out_pause/autoplay_maybe 声明 + audio_out 注释更新 + build_ncm_host.sh # SDK_SRC 补 meow_ui_audio + ncm / ncm_host # 重编产物 +新增: + bench/audiod_chain_sim.c # audiod 全链离线回归(真实 audiod_host 假 socket,14 项) + REPORT.md # 本批记录(§16) +(未动 sdk/、meow_audiod/、meow_launcherd/、vendor/;未动设备系统/恢复区) +``` + +### 16.6 诚实出入 / 已知限制 + +- **真机验收未闭环**:§16.3 内核硬挂死阻断「持续写帧 + underrun=0 + meow_audio status 在播」三项。 + 已实证到 OPEN→grant(CLI tone)与 ncm→autoplay→缓存命中→play 日志链;SDK 客户端全链 + (含抢占/重开)由宿主 sim 对真实 audiod_host 14 项覆盖。 +- **autoplay hook 默认 0 起扫前 3 个歌单首页**:若首个歌单是超大歌单(5191 首 detail ~35s 解析) + 会慢/重;文本名单点匹配(如 Docu)是推荐用法。hook 为调试资产保留,不影响正常 UI 路径。 +- **`am start dev.player.ncm` 报 pkg not registered**:amsd 二期 launcher→amsd DECLARE 转发未联调 + (WORK_STATE 已记残余),本次手动 nohup 起 ncm(fb 与 launcher 并存,测试场景可接受)。 +- 设备日志中 ipc_media META 每秒一对(state=0 空态也发)为既有行为,本批未动。 +- **未碰** /data/recovery_all、未 killall hiby_player、未主动重启设备(§16.3 的多次重启均为 + 被测路径触发的设备自重启)。 diff --git a/patch_v2/ncm_app/app/app.c b/patch_v2/ncm_app/app/app.c index e5a3f33..86b4ea5 100644 --- a/patch_v2/ncm_app/app/app.c +++ b/patch_v2/ncm_app/app/app.c @@ -84,6 +84,7 @@ static void app_on_swipe_progress(int x_start, int dx) static void app_on_tick(void) { uint64_t now = meow_ui_time_ms(); + autoplay_maybe(); /* 真机联调 hook:/tmp/ncm_autoplay 存在则自动播已缓存曲目(一次性) */ if (login_is_active()) { login_tick(now); return; } if (player_is_active()) { player_tick(now); return; } if (search_is_active()) { search_tick(now); return; } diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index f63c737..8f4d7c8 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -3,6 +3,7 @@ #include #include #include +#include #define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data" @@ -205,3 +206,89 @@ int appstate_current_playing(void) if (g_st.playing_song < 0 || g_st.playing_song >= g_st.song_count) return -1; return g_st.playing_song; } + +/* ---- 真机联调 autoplay hook(A2 音频迁移验收用,非正式能力)---- + * 触发文件 /tmp/ncm_autoplay 存在且 home 歌单已载 → 后台线程找「已缓存」曲目并播放: + * - 内容为纯数字 → 从该歌单下标起扫最多 AUTOPLAY_SCAN_PL 个歌单首页; + * - 内容为其它文本 → 按歌单名子串匹配**单个**歌单(如 "Docu";避开超大歌单的 + * 全量 trackIds 解析,真机联调省时省内存); + * - 空文件 → 从 0 起扫。 + * 风控纪律:**绝不发 song_url 下载**——无缓存命中只记日志退出;metadata 请求 = 每歌单 + * 1 detail + 1 song_detail(≤ 2×AUTOPLAY_SCAN_PL 次)。 */ +#define AUTOPLAY_TRIGGER "/tmp/ncm_autoplay" +#define AUTOPLAY_SCAN_PL 3 + +/* 播放单个歌单首页中第一首已缓存曲目。命中播放返回 1,否则 0。 */ +static int autoplay_try_playlist(int pl) +{ + int rc = appstate_load_playlist_songs_page(pl, 0, NCM_SONG_BATCH); + if (rc != NCM_OK) { + meow_log(MEOW_LOG_WARN, "autoplay: playlist[%d] load rc=%d", pl, rc); + return 0; + } + char cpath[600]; + for (int i = 0; i < g_st.song_count; i++) { + if (ncm_cache_lookup(&g_st.ctx.cache, g_st.songs[i].id, ".mp3", + cpath, sizeof cpath) == NCM_OK) { + g_st.active_playlist = pl; + meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play", + pl, i, g_st.songs[i].id); + player_manager_play(i); + return 1; + } + } + meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first page %d songs, no cached track", + pl, g_st.song_count); + return 0; +} + +static void *autoplay_worker(void *arg) +{ + const char *sel = (const char *)arg; /* NULL=从 0 起扫;数字=起始下标;其它=歌单名子串 */ + if (sel && sel[0] && !(sel[0] >= '0' && sel[0] <= '9')) { + for (int pl = 0; pl < g_st.playlist_count && !g_app_quit; pl++) { + if (strstr(g_st.playlists[pl].name, sel)) { + meow_log(MEOW_LOG_INFO, "autoplay: name '%s' -> playlist[%d] '%s' (%d tracks)", + sel, pl, g_st.playlists[pl].name, g_st.playlists[pl].track_count); + if (autoplay_try_playlist(pl)) { free((void *)sel); return NULL; } + break; + } + } + meow_log(MEOW_LOG_WARN, "autoplay: no playlist name match / no cached track ('%s')", sel); + free((void *)sel); + return NULL; + } + int start = (sel && sel[0]) ? atoi(sel) : 0; + free((void *)sel); + if (start < 0) start = 0; + int end = start + AUTOPLAY_SCAN_PL; + if (end > g_st.playlist_count) end = g_st.playlist_count; + for (int pl = start; pl < end && !g_app_quit; pl++) + if (autoplay_try_playlist(pl)) return NULL; + meow_log(MEOW_LOG_WARN, "autoplay: no cached track found (no download, risk control)"); + return NULL; +} + +void autoplay_maybe(void) +{ + static int s_fired = 0; + if (s_fired || g_app_quit) return; + if (access(AUTOPLAY_TRIGGER, F_OK) != 0) return; + if (!g_st.logged_in || g_st.playlist_count <= 0) return; /* 等 home 歌单载完 */ + s_fired = 1; + char buf[64] = ""; + FILE *f = fopen(AUTOPLAY_TRIGGER, "r"); /* 一次性:读完即删 */ + if (f) { + if (fgets(buf, sizeof buf, f)) { + size_t n = strlen(buf); + while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0; + } + fclose(f); + } + unlink(AUTOPLAY_TRIGGER); + meow_log(MEOW_LOG_INFO, "autoplay: triggered, sel='%s'", buf); + char *sel = strdup(buf); + pthread_t t; + if (pthread_create(&t, NULL, autoplay_worker, sel) == 0) pthread_detach(t); + else free(sel); +} diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index de659a8..0573604 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -5,7 +5,7 @@ * - app/ui/.c 每页一个 view 文件(login/home/playlist/player/settings) * - app/ui/common/chrome.c 共享 chrome(topbar / 内容区几何 / 空态 / 提示) * - app/logic/ appstate(全局 ctx/歌单/歌曲/播放态)+ lyric(LRC 解析) + cover(封面缓存) - * - app/play/ player_manager(播放线程) + audio_out(ALSA/文件输出) + ipc_media(媒体会话 IPC) + * - app/play/ player_manager(播放线程) + audio_out(audiod/ALSA/文件输出) + ipc_media(媒体会话 IPC) * * 铁律(APPS.md §3.4.1 / WORK_STATE.md):物理键禁映射——on_key 不处理任何键语义; * 返回=左缘左滑(on_swipe LEFT)/返回钮/meow_ui_request_exit;左缘返回:播放页→歌单→主页→退出。 @@ -267,6 +267,7 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit); void appstate_apply_playlist_cover(int idx); void appstate_set_playing_song(int idx, const char *cover_url); int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */ +void autoplay_maybe(void); /* 真机联调 hook(/tmp/ncm_autoplay 触发,自动播已缓存曲目,一次性) */ /* lyric */ int lyric_parse(const char *lrc, const char *tlrc); /* 填充 g_lyric_* */ @@ -286,9 +287,11 @@ const char *cover_current_path(void); /* 封面原始文件路径(供 la void cover_cache_reset(void); /* ---- play(app/play/)---- */ -/* audio_out */ +/* audio_out(后端:默认 SINK_AUDIOD=meow_audiod 独占服务;env NCM_AUDIO_SINK=file/null/alsa + * 或 MEOW_AUDIO_DIRECT=1 选调试后端,见 audio_out.c 文件头) */ int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功 */ int audio_out_write(const short *samples, int frames); /* <0=错误/结束 */ +int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */ void audio_out_close(void); void audio_out_set_target(int ms); /* 预留:缓冲目标 */ int audio_out_buffered_ms(void); diff --git a/patch_v2/ncm_app/app/play/audio_out.c b/patch_v2/ncm_app/app/play/audio_out.c index 640ace7..484bbbb 100644 --- a/patch_v2/ncm_app/app/play/audio_out.c +++ b/patch_v2/ncm_app/app/play/audio_out.c @@ -1,13 +1,28 @@ -/* play/audio_out.c — 音频输出抽象:ALSA 直开 hw:0,0(44.1k/16bit/立体声)或 FILE/NULL sink。 +/* play/audio_out.c — 音频输出抽象:默认 SINK_AUDIOD(meow_audiod 独占服务,经 SDK meow_audio_*), + * 调试保底 SINK_ALSADEV 直开 hw:0,0,另有 FILE/NULL sink。 + * + * 后端选择(audio_out_open,env 驱动): + * - 缺省 → SINK_AUDIOD(SDK 客户端连 /usr/data/meow_audio.sock;audiod 独占声卡、 + * 格式归一/采样率跟随/抢占全在服务端,AUDIOD_DESIGN.md §7)。 + * - NCM_AUDIO_SINK=file/null/alsa → FILE(WAV,NCM_AUDIO_OUT 指定路径,缺省 /tmp/ncm_out.wav)/ + * NULL(丢弃,仅测解码 RT)/ ALSA 直开。 + * - MEOW_AUDIO_DIRECT=1 → ALSA 直开(调试保底,非正式能力,勿依赖)。 + * audiod 不在 → open 返回 -1(不隐式回退直开,AUDIOD_DESIGN.md §7)。 + * + * ALSA 直开两大坑(真机定位,agent-246,与 audiod/SDK 同款修法,勿回退): + * ① hw_params 须「全放行」初始化(params_any 语义:mask 全 1、interval [0,UINT_MAX]) + * ——memset 0 的 interval 等价「只能取 0」→ 内核 refine 必空 → HW_PARAMS 恒 EINVAL; + * ② access/format/subformat mask 须先清零再单点(pmask 只 OR)——留全 1 会被 choose 成 + * MMAP_INTERLEAVED → write() 恒 EINVAL。 + * 注意 musl 的 sound/asound.h 中 masks/intervals 是**重索引**数组: + * masks[k] ↔ hw_param (FIRST_MASK + k);intervals[k] ↔ hw_param (FIRST_INTERVAL + k)。 * - * 设备(mipsel musl 无 libasound):用 ALSA 内核 UAPI(sound/asound.h)直开 /dev/snd/pcmC0D0p, - * 非 mmap RW 模式下 ioctl 设 hw_params + write() 写交织帧。布局/时序真机待验证(本层只保证跨编译 + - * 宿主回退正确)。 - * 宿主/qemu 验证:无 /dev/snd/pcmC0D0p → 回退 FILE sink(写 WAV,env NCM_AUDIO_OUT 指定路径, - * 缺省 /tmp/ncm_out.wav)或 NULL(env NCM_AUDIO_SINK=null,丢弃,仅测解码 RT 用)。 * 线程模型:仅由 player_manager 播放线程调用(单写者);audio_out_close 可被 UI 线程调用打断阻塞写。 + * 日志:open 结果/参数、audiod 授权 grant_*、write 错误、pause/resume/close 全写 meow_log + * (无声排查的永久资产,勿省)。 */ #include "../ncm_app_internal.h" +#include "meow_ipc.h" /* AUDIO_FMT_S16 */ #include #include #include @@ -21,11 +36,14 @@ #define NCM_HAVE_ALSA 1 #endif -enum { SINK_ALSADEV = 0, SINK_FILE = 1, SINK_NULL = 2 }; +enum { SINK_AUDIOD = 0, SINK_ALSADEV = 1, SINK_FILE = 2, SINK_NULL = 3 }; static int g_sink = -1; static int g_fd = -1; /* alsa or file fd */ +static int g_sid = 0; /* audiod stream id(>0 有效) */ static int g_rate, g_ch, g_bits; static const char *g_path = NULL; +static uint64_t g_wr_frames = 0; /* 本次 open 累计写帧(日志) */ +static int g_wr_errs = 0; /* 写错误计数(节流日志) */ #define NCM_DEFAULT_OUT "/tmp/ncm_out.wav" @@ -33,10 +51,7 @@ void audio_out_set_target(int ms) { (void)ms; } int audio_out_buffered_ms(void) { return 0; } #ifdef NCM_HAVE_ALSA -/* ALSA 非 mmap 直装:开设备 → hw_params(access=rw_interleaved, S16_LE, rate, 2ch, s16)→ write。 - * 注意:musl 的 sound/asound.h 中 masks/intervals 是**重索引**数组: - * masks[k] ↔ hw_param (FIRST_MASK + k); intervals[k] ↔ hw_param (FIRST_INTERVAL + k)。 - * 故访问时须减 FIRST_MASK/FIRST_INTERVAL,否则越界/错位。 */ +/* ALSA 非 mmap 直装:开设备 → hw_params(access=rw_interleaved, S16_LE, rate, ch)→ write。 */ 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) { @@ -49,27 +64,42 @@ static int alsa_open(int rate, int ch, int bits) { if (bits != 16) return -1; int fd = open("/dev/snd/pcmC0D0p", O_RDWR); - if (fd < 0) return -1; + if (fd < 0) { + meow_log(MEOW_LOG_ERROR, "audio: alsa open /dev/snd/pcmC0D0p failed errno=%d", errno); + return -1; + } struct snd_pcm_hw_params p; + /* 坑①:「全放行」初始化(snd_pcm_hw_params_any 语义):mask 全 1、interval [0,UINT_MAX]。 + * memset 0 的 interval 会被内核当成「只能取 0」约束 → refine 必空 → HW_PARAMS 恒 EINVAL。 */ 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; - pmask(&p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK], - SNDRV_PCM_ACCESS_RW_INTERLEAVED); - pmask(&p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK], - SNDRV_PCM_FORMAT_S16_LE); - pmask(&p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK], - SNDRV_PCM_SUBFORMAT_STD); + /* 坑②:mask 先清零再单点(pmask 只 OR):全 1 的 access 会被内核 choose 成 + * MMAP_INTERLEAVED → write() 恒 EINVAL(本机 RW_INTERLEAVED 可用,须显式约束)。 */ + 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], (unsigned)ch); 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; } + 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) { + meow_log(MEOW_LOG_ERROR, "audio: alsa HW_PARAMS failed errno=%d rate=%d ch=%d", + errno, rate, ch); + close(fd); + return -1; + } ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0); + meow_log(MEOW_LOG_INFO, "audio: alsa open ok rate=%d ch=%d (direct debug)", rate, ch); return fd; } #endif @@ -96,29 +126,76 @@ static void file_write_header(int fd, int rate, int ch, int bits, uint32_t data_ if (write(fd, h, 44) != 44) { /* 忽略 */ } } +static int file_open(int rate, int ch, int bits) +{ + int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { /* 无路可写 → null */ + meow_log(MEOW_LOG_WARN, "audio: file open %s failed errno=%d -> null sink", g_path, errno); + g_sink = SINK_NULL; g_fd = -1; + return 0; + } + file_write_header(fd, rate, ch, bits, 0); + g_sink = SINK_FILE; + g_fd = fd; + meow_log(MEOW_LOG_INFO, "audio: file sink %s rate=%d ch=%d bits=%d", g_path, rate, ch, bits); + return 0; +} + int audio_out_open(int sample_rate, int channels, int bits) { g_rate = sample_rate; g_ch = channels; g_bits = bits; + g_wr_frames = 0; g_wr_errs = 0; const char *env = getenv("NCM_AUDIO_SINK"); g_path = getenv("NCM_AUDIO_OUT"); if (!g_path || !g_path[0]) g_path = NCM_DEFAULT_OUT; - /* 显式 null(测 RT) */ - if (env && strcmp(env, "null") == 0) { g_sink = SINK_NULL; g_fd = -1; return 0; } + /* 显式 null(测解码 RT) */ + if (env && strcmp(env, "null") == 0) { + g_sink = SINK_NULL; g_fd = -1; + meow_log(MEOW_LOG_INFO, "audio: null sink rate=%d ch=%d", sample_rate, channels); + return 0; + } + /* 显式 file(宿主/回归) */ + if (env && strcmp(env, "file") == 0) + return file_open(sample_rate, channels, bits); - /* 尝试 ALSA(仅未显式指定 file;宿主无设备自动回退) */ - if (!(env && strcmp(env, "file") == 0)) { + /* 调试保底:ALSA 直开(仅显式指定;修法见文件头两大坑注释) */ + const char *de = getenv("MEOW_AUDIO_DIRECT"); + int want_alsa = (env && strcmp(env, "alsa") == 0) || (de && de[0] == '1'); + if (want_alsa) { #ifdef NCM_HAVE_ALSA int fd = alsa_open(sample_rate, channels, bits); if (fd >= 0) { g_sink = SINK_ALSADEV; g_fd = fd; return 0; } + meow_log(MEOW_LOG_ERROR, "audio: alsa direct open failed"); + return -1; +#else + meow_log(MEOW_LOG_ERROR, "audio: alsa sink requested but no ALSA on this build"); + return -1; #endif } - /* FILE sink */ - int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) { g_sink = SINK_NULL; g_fd = -1; return 0; } /* 无路可写 → null */ - file_write_header(fd, sample_rate, channels, bits, 0); - g_sink = SINK_FILE; - g_fd = fd; + + /* 默认:SINK_AUDIOD(meow_audiod 独占服务)。v1 SDK 声源格式 S16(minimp3 输出即 S16_LE)。 */ + if (bits != 16) { + meow_log(MEOW_LOG_ERROR, "audio: audiod open: bits=%d unsupported (v1 S16 only)", bits); + return -1; + } + int sid = meow_audio_open(sample_rate, channels, AUDIO_FMT_S16); + if (sid < 0) { + meow_log(MEOW_LOG_ERROR, "audio: audiod open failed rc=%d rate=%d ch=%d (audiod 不在?)", + sid, sample_rate, channels); + return -1; + } + g_sink = SINK_AUDIOD; + g_sid = sid; + /* 授权参数(grant_*)落日志:无声排查关键资产 */ + meow_audio_state st; + if (meow_audio_query(&st) == 0) + meow_log(MEOW_LOG_INFO, + "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit grant=%uHz/%uch/fmt=%u underrun=%u", + sid, sample_rate, channels, bits, st.rate, st.ch, st.fmt, st.underrun); + else + meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit", + sid, sample_rate, channels, bits); return 0; } @@ -126,6 +203,18 @@ int audio_out_write(const short *samples, int frames) { if (g_app_quit) return -1; if (g_sink == SINK_NULL || g_sink < 0) return frames; + if (g_sink == SINK_AUDIOD) { + int w = meow_audio_write(samples, frames); + if (w != frames) { + g_wr_errs++; + if (g_wr_errs == 1 || (g_wr_errs % 100) == 0) + meow_log(MEOW_LOG_ERROR, "audio: audiod write failed rc=%d frames=%d (errs=%d)", + w, frames, g_wr_errs); + return -1; + } + g_wr_frames += (uint64_t)frames; + return frames; + } size_t n = (size_t)frames * (size_t)g_ch * (size_t)(g_bits / 8); if (g_sink == SINK_ALSADEV) { /* 阻塞尽力写满 - EINTR/EAGAIN 重试;错误返回 */ @@ -135,19 +224,60 @@ int audio_out_write(const short *samples, int frames) ssize_t w = write(g_fd, p + off, n - off); if (w > 0) { off += (size_t)w; continue; } if (w < 0 && (errno == EINTR || errno == EAGAIN)) continue; + meow_log(MEOW_LOG_ERROR, "audio: alsa write failed errno=%d", errno); return -1; } + g_wr_frames += (uint64_t)frames; return frames; } if (g_sink == SINK_FILE) { if (write(g_fd, samples, n) != (ssize_t)n) return -1; + g_wr_frames += (uint64_t)frames; return frames; } return frames; } +/* 暂停(1)/恢复(0) audiod 流(停 PCM 时钟但不丢流)。仅 AUDIOD 后端有效,其余后端恒 0。 + * 恢复时若流已死(被抢占/audiod 断链)→ 自动重开(解码位置不变,仅重建输出流)。 */ +int audio_out_pause(int paused) +{ + if (g_sink != SINK_AUDIOD || g_sid <= 0) return 0; + int rc = meow_audio_pause(g_sid, paused); + if (rc >= 0) { + meow_log(MEOW_LOG_INFO, "audio: audiod %s sid=%d ok", paused ? "pause" : "resume", g_sid); + return 0; + } + if (paused) { /* 暂停失败(流多半已死):记日志即可,恢复时会走重开 */ + meow_log(MEOW_LOG_WARN, "audio: audiod pause sid=%d rc=%d (stream dead?)", g_sid, rc); + return 0; + } + /* 恢复失败 = 流已死 → 重开 */ + meow_log(MEOW_LOG_WARN, "audio: audiod resume sid=%d rc=%d -> reopen", g_sid, rc); + meow_audio_close(g_sid); + g_sid = 0; + int sid = meow_audio_open(g_rate, g_ch, AUDIO_FMT_S16); + if (sid < 0) { + meow_log(MEOW_LOG_ERROR, "audio: audiod reopen failed rc=%d rate=%d ch=%d", sid, g_rate, g_ch); + return -1; + } + g_sid = sid; + meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid); + return 0; +} + void audio_out_close(void) { + if (g_sink == SINK_AUDIOD) { + if (g_sid > 0) { + meow_audio_close(g_sid); + meow_log(MEOW_LOG_INFO, "audio: audiod close sid=%d written_frames=%llu wr_errs=%d", + g_sid, (unsigned long long)g_wr_frames, g_wr_errs); + g_sid = 0; + } + g_sink = -1; + return; + } if (g_sink == SINK_FILE && g_fd >= 0) { /* 回填:offset 4 = RIFF 总长-36;offset 40 = data 长度 */ off_t end = lseek(g_fd, 0, SEEK_END); diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index 21e11d5..ad1994f 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -26,6 +26,15 @@ static int64_t g_file_bytes; static int g_level; /* 音质档(来自设置) */ static int g_cache_gb; +/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁), + * 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */ +static void on_audio_preempt(const char *by_app, void *ctx) +{ + (void)ctx; + meow_log(MEOW_LOG_WARN, "play: preempted by [%s] -> pause", (by_app && by_app[0]) ? by_app : "?"); + player_manager_pause(1); +} + int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; } int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; } int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; } @@ -196,10 +205,12 @@ static void run_song(int idx) g_play.dur_ms = dur; g_play.err = 0; pthread_mutex_unlock(&g_play.mu); + meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s dur=%lldms", idx, s->id, (long long)dur); mp3d_sample_t pcm[PCM_CHUNK]; int64_t frames_done = 0; /* 已输出帧(每通道) */ int need_seek_ms = -1; + int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */ for (;;) { if (g_app_quit) break; @@ -212,15 +223,30 @@ static void run_song(int idx) int st = g_play.state; pthread_mutex_unlock(&g_play.mu); - if (cur != idx) break; /* 切歌 → 回外层 */ - if (st == NCM_PC_STOPPED) break; + if (cur != idx) { meow_log(MEOW_LOG_INFO, "play: switch idx=%d -> %d", idx, cur); break; } /* 切歌 → 回外层 */ + if (st == NCM_PC_STOPPED) { meow_log(MEOW_LOG_INFO, "play: stopped idx=%d", idx); break; } if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; } - if (st == NCM_PC_PAUSED) { usleep(40 * 1000); continue; } /* 冻结 */ + if (st == NCM_PC_PAUSED) { + if (!paused_edge) { /* 进暂停:停 PCM 时钟但不丢流(audiod MSG_AUDIO_PAUSE) */ + paused_edge = 1; + meow_log(MEOW_LOG_INFO, "play: paused idx=%d pos=%lldms", idx, + (long long)(frames_done * 1000 / 44100)); + audio_out_pause(1); + } + usleep(40 * 1000); + continue; /* 冻结 */ + } + if (paused_edge) { /* 出暂停:恢复 audiod 流(流死则自动重开) */ + paused_edge = 0; + meow_log(MEOW_LOG_INFO, "play: resumed idx=%d", idx); + if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; } + } if (need_seek_ms >= 0) { /* 重开并 seek:关闭后重开(字节 offset 近似) */ + meow_log(MEOW_LOG_INFO, "play: seek idx=%d -> %dms", idx, need_seek_ms); audio_out_close(); mp3dec_ex_close(&dec); memset(&dec, 0, sizeof dec); @@ -235,7 +261,10 @@ static void run_song(int idx) } size_t got = mp3dec_ex_read(&dec, pcm, PCM_CHUNK); - if (got == 0) break; /* 播完 */ + if (got == 0) { /* 播完 */ + meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done); + break; + } int frames = (int)(got / 2); if (audio_out_write(pcm, frames) != frames) break; frames_done += frames; @@ -377,6 +406,7 @@ int player_manager_start(void) g_level = g_st.level; g_cache_gb = g_st.cache_limit_gb; pthread_mutex_init(&g_play.mu, NULL); + meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */ if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1; g_thr_on = 1; return 0; diff --git a/patch_v2/ncm_app/bench/audiod_chain_sim.c b/patch_v2/ncm_app/bench/audiod_chain_sim.c new file mode 100644 index 0000000..90b88a2 --- /dev/null +++ b/patch_v2/ncm_app/bench/audiod_chain_sim.c @@ -0,0 +1,182 @@ +/* bench/audiod_chain_sim.c — ncm 音频输出迁移(A2,SINK_AUDIOD)离线全链验证(0 网络)。 + * + * 起**真实** meow_audiod_host(FILE sink,假 socket=/tmp/ncm_audiod_sim.sock)作服务端, + * 驱动**真实** app/play/audio_out.c(SINK_AUDIOD 默认后端)+ SDK 客户端 meow_ui_audio.c: + * ① audio_out_open(44100,2,16) → audiod OPEN/OPEN_ACK(grant 授权) + * ② 连续 WRITE(正弦帧)→ meow_audio_query 见 active=1 / written>0 + * ③ audio_out_pause(1)/(0) → QUERY paused=1/0(停 PCM 时钟不丢流) + * ④ 第二客户端裸发 OPEN 抢占 → on_preempt 回调触发 + 旧流 write 失败 + * ⑤ audio_out_pause(0) 对流死自动重开 → 恢复可写 + * ⑥ audio_out_close → QUERY active=0(流回收);audiod 侧 WAV 落盘非空 + * + * 编译(宿主;需先 ../meow_audiod/build_audiod.sh host 产出 meow_audiod_host): + * cc -O2 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ + * -Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3 \ + * bench/audiod_chain_sim.c app/play/audio_out.c \ + * ../sdk/meow_ui_audio.c ../sdk/meow_ui_log.c -o audiod_chain_sim -lpthread -lm + * 运行:./audiod_chain_sim (PASS/FAIL 计数,全过 exit 0) + */ +#include "../app/ncm_app_internal.h" /* audio_out_* + meow_ui.h */ +#include "meow_ipc.h" /* 抢占者裸帧:MSG_AUDIO_OPEN/m_audio_open_t */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- audio_out.c / meow_ui_log.c 依赖的外部符号(本 sim 提供最小实现) ---- */ +volatile int g_app_quit = 0; +uint64_t meow_ui_time_ms(void) +{ + struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000; +} + +#define SIM_SOCK "/tmp/ncm_audiod_sim.sock" +#define SIM_WAV "/tmp/ncm_audiod_sim.wav" + +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++; } +} + +/* 抢占回调标志(SDK 读线程置位) */ +static volatile int g_preempted = 0; +static void sim_preempt_cb(const char *by_app, void *ctx) +{ + (void)ctx; + printf(" [cb] preempted by %s\n", by_app ? by_app : "?"); + g_preempted = 1; +} + +/* ---- 起 audiod(FILE sink 假声卡) ---- */ +static pid_t g_srv = -1; +static int start_audiod(void) +{ + unlink(SIM_SOCK); unlink(SIM_WAV); + pid_t p = fork(); + if (p == 0) { + setenv("MEOW_AUDIO_SOCK", SIM_SOCK, 1); + setenv("MEOW_AUDIO_OUT", SIM_WAV, 1); + setenv("MEOW_AUDIO_SINK", "file", 1); + setenv("MEOW_AUDIO_FORCE", "1", 1); + setenv("MEOW_AUDIO_AMIXER", "/bin/true", 1); + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); } + execl("../meow_audiod/meow_audiod_host", "meow_audiod_host", (char *)NULL); + _exit(127); + } + if (p < 0) return -1; + g_srv = p; + for (int i = 0; i < 50; i++) { /* 等 socket 出现(≤5s) */ + if (access(SIM_SOCK, F_OK) == 0) return 0; + usleep(100 * 1000); + } + return -1; +} + +/* ---- 抢占者:裸 IPC 客户端发 OPEN ---- */ +static int wr_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 intruder_open(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", SIM_SOCK); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + m_audio_open_t o; + memset(&o, 0, sizeof o); + snprintf(o.app_id, sizeof o.app_id, "intruder"); + o.rate = 44100; o.ch = 2; o.fmt = AUDIO_FMT_S16; + uint8_t hdr[16]; + uint32_t magic = MEOW_IPC_MAGIC, len = sizeof o, req = 99; + uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_OPEN; + memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &type, 2); + memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &len, 4); + if (wr_all(fd, hdr, 16) != 0 || wr_all(fd, &o, sizeof o) != 0) { close(fd); return -1; } + return fd; /* 保持连接(断开会 EOF 回收 intruder 流) */ +} + +int main(void) +{ + setenv("MEOW_AUDIO_SOCK", SIM_SOCK, 1); /* SDK 客户端连假 socket */ + setenv("MEOW_LOG_APP", "dev.player.ncm", 1); /* audiod 状态里的 app_id */ + unsetenv("NCM_AUDIO_SINK"); /* 确保走默认 SINK_AUDIOD */ + unsetenv("MEOW_AUDIO_DIRECT"); + + if (start_audiod() != 0) { printf("FAIL: audiod_host start\n"); return 1; } + printf("audiod up (sock=%s)\n", SIM_SOCK); + + /* ① OPEN */ + meow_audio_on_preempt(sim_preempt_cb, NULL); + check(audio_out_open(44100, 2, 16) == 0, "open: SINK_AUDIOD default ok"); + + /* ② WRITE 连续帧(~1s 正弦)+ QUERY 见活跃流 */ + static short pcm[2048 * 2]; + int wr_ok = 1; + for (int c = 0; c < 25 && wr_ok; c++) { + for (int i = 0; i < 2048; i++) { + double t = (double)(c * 2048 + i) / 44100.0; + short v = (short)(sin(2.0 * 3.14159265 * 440.0 * t) * 12000); + pcm[i * 2] = v; pcm[i * 2 + 1] = v; + } + if (audio_out_write(pcm, 2048) != 2048) wr_ok = 0; + usleep(10 * 1000); + } + check(wr_ok, "write: 25 chunks x 2048 frames ok"); + meow_audio_state st; + check(meow_audio_query(&st) == 0 && st.active == 1 && st.written_frames > 0, + "query: active=1 written>0"); + printf(" [state] app=%s sid=%u rate=%u ch=%u fmt=%u written=%llu played=%llu underrun=%u paused=%d\n", + st.app_id, st.stream_id, st.rate, st.ch, st.fmt, + (unsigned long long)st.written_frames, (unsigned long long)st.played_frames, + st.underrun, st.paused); + + /* ③ PAUSE / RESUME(停时钟不丢流) */ + check(audio_out_pause(1) == 0, "pause(1) ok"); + usleep(100 * 1000); + check(meow_audio_query(&st) == 0 && st.paused == 1, "query: paused=1"); + check(audio_out_pause(0) == 0, "resume(0) ok"); + usleep(100 * 1000); + check(meow_audio_query(&st) == 0 && st.paused == 0, "query: paused=0"); + + /* ④ 抢占:intruder OPEN → 回调触发 + 本流写失败 */ + int ifd = intruder_open(); + check(ifd >= 0, "intruder: open sent"); + for (int i = 0; i < 30 && !g_preempted; i++) usleep(100 * 1000); + check(g_preempted == 1, "preempt: on_preempt callback fired"); + check(audio_out_write(pcm, 128) < 0, "preempt: old stream write fails"); + + /* ⑤ 流死后 resume(0) 自动重开(抢回流)→ 恢复可写 */ + check(audio_out_pause(0) == 0, "resume-after-preempt: reopen ok"); + check(audio_out_write(pcm, 2048) == 2048, "reopened stream writable"); + if (ifd >= 0) close(ifd); + + /* ⑥ CLOSE → 流回收 */ + audio_out_close(); + usleep(200 * 1000); + check(meow_audio_query(&st) == 0 && st.active == 0, "close: query active=0"); + + struct stat sb; + check(stat(SIM_WAV, &sb) == 0 && sb.st_size > 44, "audiod WAV non-empty"); + + if (g_srv > 0) { kill(g_srv, SIGTERM); waitpid(g_srv, NULL, 0); } + printf("== audiod_chain_sim: pass=%d fail=%d ==\n", g_pass, g_fail); + return g_fail ? 1 : 0; +} diff --git a/patch_v2/ncm_app/build_ncm_host.sh b/patch_v2/ncm_app/build_ncm_host.sh index f3c63ba..b18238f 100755 --- a/patch_v2/ncm_app/build_ncm_host.sh +++ b/patch_v2/ncm_app/build_ncm_host.sh @@ -19,7 +19,8 @@ INC="-I. -I../sdk -I../meow_launcherd -Icore \ echo "==> host SDK objects" SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \ meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \ - meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin" + meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \ + meow_ui_audio" for f in $SDK_SRC; do $CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \ "$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; } diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 3b3db7d..146ac54 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/ncm_app/ncm_host b/patch_v2/ncm_app/ncm_host index 086bfd7..1fba9e4 100755 Binary files a/patch_v2/ncm_app/ncm_host and b/patch_v2/ncm_app/ncm_host differ