# os.meow.ncm 技术栈预研 + 构建验证报告 > 日期:2026-08-31。工作区:`/home/Aska/Project/R3ProII`,仅改动 `patch_v2/ncm_app/`。 > 目标:为 MeowOS 网易云 App(v1 = 扫码登录 + 我的歌单 + 播放)在 HiBy R3 Pro II > (MIPS32r2 单核、~14MB 可用内存、无 GPU、ALSA 直开 hw:0,0)上的纯 C 静态栈做选型与真机交叉编译验证。 > 工具链:`.tools/zig-mipsel-cc`(zig 包装 mipsel-linux-musleabi soft-float 静态交叉)。全程未 commit/push/刷机/动设备。 ## 1. 选型结论表 | 能力 | 选型 | 版本 | 许可 | mipsel 体积(text) | 说明 | |---|---|---|---|---|---| | TLS + 加密 | **mbedtls(裁剪)** | 3.6.2 | Apache-2.0 | 链接 Demo ≈ **388 KB** | TLS1.2 client + AES-128-CBC/ECB/GCM + MD5 + RSA(1024 raw) + Base64 + X509,单库全包 | | TLS(备选) | BearSSL | master@7bea48e | MIT | TLS client 子集 ≈ **96 KB** | 更省 flash,但无 base64、AES-ECB 需自行逐块、RSA raw 用 `br_rsa_i31_public` | | JSON | **jsmn** | 1.1.0 | MIT | **2.2 KB** | 分词器,无 malloc/无 DOM;cJSON 对比 20 KB | | 二维码 | **qrcodegen** | nayuki@993ccbb | MIT | **10.8 KB** | 纯 C 单文件,无图形依赖 | | MP3 解码 | **minimp3** | master@ea99364 | CC0 | **37.4 KB** | `minimp3_ex` 流式/可 seek;dr_mp3 对比 40.7 KB | **HTTPS GET/POST 可达性**:mbedtls 已实测连通(见 §2)。**crypto 是否 mbedtls 自带就够**:是—— AES-128-ECB/CBC、MD5、Base64、RSA(raw) 全有;RSA raw 用 `mbedtls_rsa_public()`(无 padding 纯模幂)已交叉校验与 `pow(m,e,n)` 完全一致(见 §3),这是文档标注最易踩坑处,已验证可行。 ## 2. HTTPS 实测(宿主 = 本机 aarch64;另跑通 MIPS 版) **demo**:`demo/https_unikey.c`(mbedtls TLS1.2 + eapi 加密 + jsmn)。 目标:`POST https://interface.music.163.com/eapi/login/qrcode/unikey`,body `params=`。 ``` deviceId=68DFA27096B7A5DC2CB393C77D8C194FDC0459B743081D091EBD ---- response body ---- {"code":200,"unikey":"33a1c6a6-28ad-4e1e-bc8e-ebe769953913"} UNIKEY = 33a1c6a6-28ad-4e1e-bc8e-ebe769953913 ``` - 宿主:`code:200` + unikey 顶层字段,与 NCM_API_NOTES §7 一致 ✅ - **MIPS 版交叉编译后,在 `qemu-mipsel` 下全链路跑通**(DNS→TCP→TLS→POST→JSON 解析),同样拿到真实 unikey: `{"code":200,"unikey":"6731b1ac-71a2-41eb-a704-a2de39d6d0b7"}`。即 mipsel 静态产物的网络+crypto+TLS 路径正确。 - eapi 加密逻辑与原版 `crypto::eapi` 一致:`digest=md5("nobody"+url+"use"+json+"md5forencrypt")`, `plain=url+"-36cd479b6b5-"+json+"-36cd479b6b5-"+digest`,`params=HEX_UPPER(AES128ECB(plain,"e82ckenh8dichen8"))`; `header`(osver/deviceId/os/appver/versioncode/mobilename/buildver/resolution/__csrf/channel/requestId)与 `e_r:false` 均入加密体(§8 要求,去掉会 4xx)。 ## 3. RSA raw(weapi 无 padding 模幂)校验 `mbedtls_rsa_public(ctx, 128B输入右对齐16B明文, 128B输出)`,n=1024-bit、e=65537。 - mipsel(qemu) 与宿主输出一致,且与 Python `pow(m,e,n)` 逐一字节相同(256 hex 完全匹配): `CT = 55B33E7E3229B8D977137D120B3C7A5B5FB8E933325CAA5BB8CEF6C132D57E5E...B2B83` - 结论:**mbedtls_rsa_public() 可用作 weapi 的 textbook RSA**,避开 PKCS#1 padding 陷阱。demo:`demo/rsa_raw_test.c`。 ## 4. 总体积账(mipsel,-Os,flash≈text) **选定的库合计(≈438 KB)**: | 项 | text | 备注 | |---|---|---| | mbedtls TLS+crypto+x509(链接 demo) | 388 KB | 含 ~6 KB musl libc 基线;库文本合计 410 KB,gc-sections 未再降 | | jsmn | 2.2 KB | | | qrcodegen | 10.8 KB | | | minimp3 | 37.4 KB | | | **库合计** | **≈ 438 KB** | 另需 App/UI/封面/音频输出层(未计入,自研) | 可选裁剪方向(能明显压 mbedtls):限制密码套件到 `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`(+1 个 CBC+SHA 兜底)、 ECP 只留 P-256、去掉 ECDSA(服务器给 RSA 证书)、`MBEDTLS_SSL_IN/OUT_CONTENT_LEN=4096`。估可降到 ~250 KB。 若固件预算仍紧,**切 BearSSL(≈96KB) + 自带 base64/AES-ECB 例程**,能再省 ~300 KB。 **对比参考项体积**:cJSON 20 KB、dr_mp3 40.7 KB、BearSSL 全库 310 KB(裁剪子集 96 KB)。空静态 mipsel 基线 text=5888。 ## 5. 内存 / 性能估算(14MB 可用) - **TLS 握手 buffer**:mbedtls 默认 `SSL_IN/OUT_CONTENT_LEN`=16KB 各一,共 ~32KB;demo 二进制 `bss=26888`(≈26KB)+堆内 SSL context/ECDH 临时密钥。对 14MB 无压力;建议调成 4096 更省。 - **JSON 解析**:jsmn 无 malloc,token 数组按需分配(响应小,几百字节)。**不要全量缓冲歌单详情**,只取需要的键(文档 §4.4)。 - **解码**:minimp3 流式,输入 ~每帧几 KB + 输出 `MINIMP3_MAX_SAMPLES_PER_FRAME*2*ch`,整体 <20KB。 - **封面**:**必须限幅**——NCM 建议 5MB,MeowOS 应限 ~256KB 并降采样/调色板(无 GPU 只能 CPU 软缩放)。 - **二维码位图**:41×41 符号渲染成 UI 尺寸(如 400×400,灰度 1B/px=160KB,1-bit 可 20KB)。 - 预算结论:TLS+解码+二维码+封面(256KB cap)+UI 帧缓冲,**总工作集应在数百 KB 量级,14MB 富余**;唯一要盯的是封面/歌单响应别全量驻留。 - **性能**: - **MP3 解码实时性**:宿主(aarch64) minimp3 = **568×RT**、dr_mp3 = 627×RT; `qemu-mipsel`(TCG 模拟,**远慢于真机**,通常 10–50×) 下 minimp3 = **4.8×RT**、dr_mp3 = 4.7×RT。 → 真机 MIPS32 单核跑 44.1k/64kbps 解码**余量很大**(估计数十倍 RT),解码不是瓶颈。⚠️ 该数字为宿主/模拟估算,标注不准。 - **TLS 握手**:ECDHE-RSA-AES128-GCM,单核软 RSA 1024 校验 + ECDH P-256,估几十~几百 ms,可接受(仅首次/重连)。 - **weapi RSA raw 模幂**:1024-bit 软模幂每次约几 ms~几十 ms(只对 weapi 请求,频率低)。 ## 6. 风险与 fallback | 风险 | 评估 | 对策/fallback | |---|---|---| | weapi RSA raw 最易踩坑 | ✅ 已交叉校验正确 | 保持 `mbedtls_rsa_public()`+左补零到 256 hex;用 `rsa_raw_test` 做回归 | | deviceId/UA 不稳定触发风控 | 高 | deviceId(52 hex) **首生成后持久化**;UA 按 eapi(iPhone)/weapi(Chrome) 分别固定;轮询≥1s、失败退避 | | session(MUSIC_U)丢失即需重扫码 | 高 | cookie 持久化;启动用 `/weapi/w/nuser/account/get` 校验 | | eapi 的 `header`/`e_r` 缺失 → 4xx | 中 | demo 已带,必须保留(§8) | | 无版权/VIP 曲目 url=null | 中 | `song_url_v1` 返回 null → 提示 + 降级到 `song/enhance/player/url`(br=320000) 兜底 | | 音质策略(Hi-res 降级) | v1 只保 mp3 | 账号非免费档 level 被钳到免费档;v1 默认 `standard`(128k)/`higher`(192k);**不引入 flac 解码**(14MB 内存+单核不划算);后续再评估 dr_flac | | 触发高频风控 | 中 | 请求限频、重试退避、必要时随机国内 IP(`X-Real-IP`/`X-Forwarded-For`) | | mbedtls flash ~388KB 偏大 | 中 | 见 §4 裁剪;若预算吃紧切 BearSSL(≈96KB) | ## 7. 出入清单(只改 `patch_v2/ncm_app/`) ``` patch_v2/ncm_app/ ├── vendor/ # 各库源码(上游 clone/tar + 许可证) │ ├── mbedtls/ (3.6.2, 含 framework 子模块, 裁剪 config 在 config/mbedtls_config_ncm.h) │ ├── bearssl/ (官方 git, 无语义版本) │ ├── jsmn/ cjson/ qrcodegen/ minimp3/ dr_mp3/ ├── libs_common.sh # 公共 zig/ar/size 环境 ├── build_mbedtls.sh # 裁剪 config + 静态编 mipsel → out/mbedtls/mipsel/*.a ├── build_bearssl.sh # make lib → out/bearssl/mipsel/bm/libbearssl.a ├── build_cjson.sh build_jsmn.sh build_qrcodegen.sh build_minimp3.sh build_dr_mp3.sh ├── build_demo.sh # 宿主 + mipsel 的 HTTPS demo 复现 ├── bench/ # minimp3/dr_mp3 基准(sample-15s.mp3 来自 samplelib) ├── demo/https_unikey.c # eapi + TLS1.2 + jsmn(打 unikey) ├── demo/rsa_raw_test.c # weapi RSA raw 校验 ├── out/https_unikey_mipsel(. _s) # mipsel 静态 demo(qemu-mipsel 跑通) ├── out/mbedtls/{mipsel,host}/ # 库产物 out/bearssl/mipsel/bm/ ├── VENDOR_NOTES.md # 版本/许可/裁剪/体积 └── REPORT.md # 本报告 ``` **未做/待主会话**:真机 `hw:0,0` 播放链路、扫码后真实登录(803+set-cookie)、weapi 真实登录态请求、 CA bundle 内置方案、音质档位映射实测。这些属设备/账号侧联调,由主会话在设备上执行(本预研不动设备)。 ## 8. 结论 - **可行**:纯 C + mbedtls(裁剪) + jsmn + qrcodegen + minimp3,静态 mipsel 交叉编译全通,HTTPS POST 拿到真实 unikey,MIPS(qemu) 版全链路跑通。 - **推荐 TLS/加密一体用 mbedtls**(单库一次解决 TLS + AES/MD5/RSA raw/Base64 + X509,规避 RNSS-raw 手写风险);BearSSL 为省 flash 备选。 - **v1 体积增量 ≈ 438 KB(库)/ 只做 mp3**,内存估计数百 KB 工作集,14MB 可行;主要优化点是 mbedtls 裁剪与封面限幅。 - **最关键风险(RSA raw)已验证**;其余风险集中在风控(deviceId/UA 稳定持久化)与无版权降级策略。 --- ## 9. 核心库批次(core/,纯 C 静态库,无 UI) > 日期:2026-08-31。在 §1-8 预研(候选库/工具链已定型)基础上落地核心库:http/crypto/login/api/cache 五模块 > + 公共/存储/JSON/请求构建支撑模块 + ncm_ctx 上下文。仅改动 `patch_v2/ncm_app/`。未 commit/push/刷机/动设备。 ### 9.1 模块与公开 API(头文件签名) | 模块 | 文件 | 主要公开函数 | |---|---|---| | 公共工具 | `core/ncm_common.h/.c` | `ncm_hex_encode/decode` `ncm_b64_encode/decode` `ncm_md5_hex` `ncm_sha256_hex` `ncm_url_encode/decode` `ncm_random_bytes` `ncm_now_ms` 等 | | JSON | `core/ncm_json.h/.c` | `ncm_json_parse` `ncm_json_obj_get` `ncm_json_arr_get/len` `ncm_json_path[_str/_int/_bool]` `ncm_json_get_str_at/get_int_at` | | HTTP | `core/ncm_http.h/.c` | `ncm_http_request`(一次性 POST/GET,限幅)`ncm_http_download`(流式回调 + max_bytes)`ncm_http_parse_set_cookie` `ncm_http_set_verify_default` | | 存储 | `core/ncm_store.h/.c` | `ncm_store_init` `ncm_store_get/set_device_id` `ncm_store_machine_key` `ncm_store_cookie_encrypt/decrypt` `ncm_store_save/load/clear_session` `ncm_store_write/read_file` `ncm_store_read_cfg_int` | | 加密 | `core/ncm_crypto.h/.c` | `ncm_crypto_eapi` `ncm_crypto_weapi` `ncm_crypto_linuxapi` `ncm_crypto_rsa_raw_selfcheck` | | 请求构建 | `core/ncm_req.h/.c` | `ncm_req_build`(eapi/weapi/linux,注入 header/csrf_token/UA/Cookie)`ncm_req_cookie_get` | | 登录 | `core/ncm_login.h/.c` | `ncm_login_qr_url` `ncm_login_unikey` `ncm_login_qr_matrix`(qrcodegen)`ncm_login_poll` `ncm_login_status_check` `ncm_login_save/load/clear_cookie` | | 歌单/播放 | `core/ncm_api.h/.c` | `ncm_api_user_playlist` `ncm_api_playlist_detail` `ncm_api_song_detail` `ncm_api_song_url` `ncm_api_song_url_auto` `ncm_api_lyric` `ncm_api_level_str` | | 缓存 | `core/ncm_cache.h/.c` | `ncm_cache_init` `ncm_cache_set_limit_gb/limit_gb` `ncm_cache_load_setting` `ncm_cache_lookup` `ncm_cache_open/write/commit/abort` `ncm_cache_total_bytes` `ncm_cache_evict` `ncm_cache_path` | | 上下文 | `core/ncm.h` + `ncm_ctx.c` | `ncm_ctx_t`(deviceId/cookie/CA/默认音质/cache)`ncm_ctx_init` `ncm_ctx_set/load/clear_cookie` `ncm_ctx_default_cache_dir` | 关键常量:`NCM_EAPI_KEY`/`NCM_WEAPI_PRESET`/`NCM_WEAPI_IV`/`NCM_LINUXAPI_KEY`/`NCM_RSA_DER_B64`(`ncm_crypto.h`); UA 按加密类型分流(eapi=iPhone `NeteaseMusic 9.0.90/5038`,weapi=Chrome/Edg 124),请求头注入对齐 CNMPlayer `request.rs`(deviceId/UA 稳定)。 ### 9.2 关键实现取舍 - **CA 方案**(`ncm_http.h` 注释):`verify_ca=1`(默认)加载系统 CA bundle(或 `ca_path`),失败即拒绝(防 MITM);`verify_ca=0` 跳过校验(仅调试/受限网)。全局默认由 `ncm_http_set_verify_default` 设定,`ncm_ctx_init` 以其 `verify_ca/ca_path` 设置。**设备侧需内置 CA 根**(VENDOR_NOTES §备注),否则接口 eapi/weapi 会被拒。 - **RSA raw**:用 `mbedtls_rsa_public()` 无 padding 纯模幂,16 字节 secret 右对齐 128 字节(前导 0),输出 256 小写 hex——已与 Python `pow(m,e,n)` 逐字节一致(见 9.4)。 - **weapi**:双层 AES-128-CBC(`params` base64,含 `+/=` 需 urlencode)+ RSA(raw) 加密随机 16 位 base62 密钥(RSA 前**反转**)。 - **会话cookie**:加密落盘 `MeowData//data/session.enc`,密钥 = `sha256(MAC + 盐)`;无 MAC(qemu/无网卡)回退到设备私有 `.mkey` 派生。**仅为防裸读模糊,非真安全**(头文件已注明)。 - **缓存**:目录默认 `MeowData/os.meow.ncm/cache/`;上限默认 10GB(1~100 可配),读设置键 `ncm.cache_limit_gb`(`/ncm.cache_limit_gb.cfg`,对齐 SDK meow_config);LRU 按文件 mtime,`ncm_cache_lookup/commit` 刷新;命中即离线可播。 - **音质**:`ncm_api_song_url_auto` 从 desired(默认 Hi-res)往下降级 `hires→lossless→exhigh→higher→standard`,只返回可解码 mp3(url 非空且 type=mp3);无该档对当前歌曲降级。 ### 9.3 构建 `./build_core.sh` → `out/core-host/`、`core-mipsel/`(自有代码对象 + vendor qrcodegen + jsmn 唯一实现 TU),链接 `out/mbedtls/{host,mipsel}/*.a`,产出: - `test/test_ncm_core_host`(宿主,可跑) - `out/test_ncm_core_mipsel`(mipsel 静态)。`size`:`text 462662 / data 212 / bss 27688`(含测试 printf 基建 + mbedtls + qrcodegen;核心库本身链接更小)。 - 自有代码 `-Wall -Wextra` **0 warning**;vendor 警告豁免。mipsel 用 `.tools/zig-mipsel-cc` 静态交叉。 ### 9.4 验证记录(宿主 + qemu-mipsel 双向实测,响应已脱敏/截断) - **unikey 实拿(eapi)**:`{"code":200,"unikey":""}`(宿主 `dc5bd090-…`,qemu-mipsel `ef9a5db2-…`);二维码 URL `https://music.163.com/login?codekey=`。 - **轮询(未扫码)**:`{"code":801,"message":"等待扫码"}` → `state=等待扫码(801)`,两平台一致。 - **login_status(伪造 cookie)**:返回 weapi 可解析 JSON(code 200 但 profile null)→ 库判 `NCM_ERR_AUTH(-13)`,证明 weapi 请求被服务端接受。 - **RSA raw 交叉**:`ncm_crypto_rsa_raw_selfcheck` 输出 `55b33e7e…b2b83`,与 Python `pow(m,65537,n)`(§3 已交叉验证值)**逐字节一致**。weapi params 为合法 base64(RSA 封装的 16 字节随机密钥无法无私钥解密,其 AES-CBC 为标准用法)。 - **eapi 往返**:`ncm_crypto_eapi` 产物可用 AES-ECB 解回含 `url` + `json` 的明文(`/api/login/qrcode/unikey` + `{"type":3}`)。 - **cookie 加解密**:38 字节串 `MUSIC_U=…; __csrf=xyz; os=pc` 加密→解密完全一致(含一次 mbedtls `crypt_cbc` 就地改写 IV 的坑,已修)。 - **缓存**:写入/命中/abort 不留档/强制小上限触发 LRU 淘汰(总字节降至 ≤ 上限)。 - **二维码**:41×41 内矩阵生成,暗模块 >0。 - **无凭据错误处理**:`user_playlist(uid=0,无cookie)` 优雅返回(公开数据或错误码,不崩溃);`song_url(id=0)` 返回无 url。 ### 9.5 md5(自有源码) ``` 778847a8419325af2b95ad0ef7749781 core/ncm_api.h c07d246c9b64e4d3b9078b9e33d19fe4 core/ncm_cache.h 6e878566f6a4c4377c98e87e9967fdcc core/ncm_common.h 9056253bfb635d8224e7454c6b1796a2 core/ncm_crypto.h 157807f07a96f43249f6aeb9707d053f core/ncm.h 477dd9f5eab61e2940cb432d1b215b3a core/ncm_http.h 398e62a0b641f13bc80c02d5e42a0401 core/ncm_json.h 05fb336d3b65b041095bd8fb3c64bc3c core/ncm_login.h 96f423ef1a36bda7b5cb3961fbe22028 core/ncm_req.h 02c7ff38dd856455f32a4cc7bbc5edd5 core/ncm_store.h f8f8e96464d3c4ebcf1898eb4212c55a core/jsmn_impl.c a00e9b9b1adf7c6cf924eb9ecfc90a67 core/ncm_common.c e75c29217ef5dd3f0e23c740b79210c1 core/ncm_crypto.c 9ebfa83e31a530b4dc8fd7877c2f5fb9 core/ncm_ctx.c 319df449ecd7727f328e01adb518ba57 core/ncm_http.c d69ff302ec111a74f621571818da4622 core/ncm_json.c a53e55f9cab828fd59b694330ec7371e core/ncm_login.c d81eda09640ff748b0a1c14607447d1b core/ncm_req.c 1ceda66d30d44a3d8850df2e876c0195 core/ncm_store.c 7cdb84fcf87384eb5125e5aae01eac84 test/test_ncm_core.c 94a4dd07fba98a6e7b70021aaeb8cb19 build_core.sh ``` 产物:`test/test_ncm_core_host` md5 `0e41d3d413819768d65d2e9ca9e082ed`;`out/test_ncm_core_mipsel` md5 `e05f1937e5fc49a2590b830c28ee6372`。 ### 9.6 出入清单(只改 `patch_v2/ncm_app/`) ``` 新增: core/ncm.h ncm_ctx.c // 伞头 + 上下文 core/ncm_common.h/.c // hex/base64/md5/sha256/urlencode/errcode core/ncm_json.h/.c // jsmn 路径导航 core/ncm_crypto.h/.c // eapi/weapi/linuxapi + RSA raw core/ncm_http.h/.c // mbedtls HTTPS POST/GET + CA取舍 + 超时 + chunked core/ncm_store.h/.c // deviceId/MAC机器密钥/cookie加解密/配置读取 core/ncm_req.h/.c // 加密请求构建(md5/UA/cookie 注入) core/ncm_login.h/.c // unikey/二维码矩阵/轮询状态机/cookie捕获/login_status core/ncm_api.h/.c // 歌单/详情/歌曲/播放地址/歌词 core/ncm_cache.h/.c // LRU 缓存 + 离线可播 core/jsmn_impl.c // jsmn 唯一实现 TU(防多 TU 重复定义) test/test_ncm_core.c // 宿主 + mipsel 综合实测 build_core.sh // 宿主 + mipsel 编译链接 修改: REPORT.md // 追加本批记录 (未动 vendor/,未动其余目录) ``` ### 9.7 未做/诚实出入 - **803 真登录**未实测(需真实扫码):code 803 分支的 set-cookie 捕获/合并逻辑按 document 实现,但未真跑;联调时建议真扫码验证一次 `MUSIC_U` 内容与 `login_status` 校验链路。 - **weapi params 无法无 RSA 私钥解密**核验其内部 AES 双层,但 RSA 封路径已逐字节交叉,AES-CBC 为标准用法(可用 CNMPlayer 同源实现比对)。 - **播放地址/缓存仅验证到“请求-解析-无版权返回 null”**;未做真实音频下载落盘(缓存 open/write/commit 已由单元测试覆盖;真实 mp3 下载留给带 UI 的播放层用 `ncm_http_download`+`ncm_cache_*` 串联)。 - **栈用量**:单次 http 调用涉及 mbedtls ssl/净栈,峰值栈估几十 KB(与预研 demo 同级);App 线程栈需 ≥64KB,联调时以真机线程栈为准。 - **JSON 响应限幅**:歌单/歌曲详情走 1MB~2MB 堆缓冲 + token 计数后精确分配,不算常驻;封面/大 JSON 不整体驻留,封面下载另有 `max_bytes` 限幅(建议 ~256KB,未在本次接线)。 --- ## 10. 网易云客户端 v1 = App + UI + 播放链路(`os.meow.ncm`) > 日期:2026-08-31。在 §9 核心库之上落地 App:UI(登录/歌单/播放/设置)+ 播放链路(URL 降级→缓存→minimp3→audio_out)+ 媒体会话 IPC。 > 仅改动 `patch_v2/ncm_app/app/`;未动 sdk/、meow_launcherd/(并行域冻结);未碰 vendor/、core/(§9 核心库原样使用)。 > 宿主 + qemu-mipsel 双边实测见 §10.5。未 commit/push/刷机/动设备。 ### 10.1 包结构与页面 ``` patch_v2/ncm_app/ ├── app.json # {"name":"网易云音乐",exec:.../os.meow.ncm/ncm,package,version:0.1,version_code:1,framework:1,immersive:1,fonts:gbk} ├── build_ncm.sh # mipsel 静态 0 warning ├── build_ncm_host.sh # 宿主离屏自测(MEOW_UI_DUMP) ├── ncm # mipsel 静态 App(md5 见 §10.6) ├── app/app.c # main + 装配 + 事件分发(物理键不映射;左缘左滑=返回/退出) ├── app/ncm_app_internal.h # 内部共享头(ACT_*/布局/数据上限/播放运行态/各页与 play/ipc 接口) ├── app/ui/ │ ├── chrome.c # TopAppBar/内容区几何/空态/页脚/底部提示条(ncm_snack) │ ├── login.c # 登录页:后台 worker: unikey→二维码矩阵→轮询→803 落 cookie→login_status │ ├── home.c # 我的歌单(meow_listview:folder图标+名称+曲目数副标题+箭头) │ ├── playlist.c # 歌单详情(曲目列表:歌名+歌手;点击起播并推进播页) │ ├── player.c # 播放页(封面+标题/歌手+进度条seek+播放/暂停/上下曲+滚动歌词;竖/横布局可切) │ └── settings.c # 设置(音质 Hi-res/较高/标准 + 缓存上限 1~100GB + 退出登录) ├── app/logic/ │ ├── appstate.c # ctx/cookie/uid/歌单/歌曲/设置加载与持久化(meow_config_*) │ ├── lyric.c # LRC 时间轴解析(原文+翻译合并、排序、行定位;流式有界 NCM_LYRIC_LINES) │ └── cover.c # 封面后台线程下载(≤288KB)+解码单张缓存 + 落盘 cover_current.jpg 供 launcher ├── app/play/ │ ├── player_manager.c # 播放线程:URL 降级→缓存(命中即播/否则流式落盘)→minimp3→audio_out;暂停冻结/seek重开/上下曲 │ ├── audio_out.c # 音频输出:ALSA 直开 hw:0,0(内核UAPI) / FILE(WAV) / NULL(测RT) │ ├── ipc_media.c # 媒体会话 IPC:MSG_META 上报 + MSG_PLAYCTL 接收(meow_ipc.h 直接帧格式) │ └── minimp3_impl.c # minimp3_ex 唯一实现 TU └── app/test/ ├── test_ncm_app.c # 宿主真实页面渲染(NCM_PAGE=login|loginlive|home|playlist|player|lands|settings) └── test_decode.c # 解码链路(minimp3→audio_out FILE)验证 ``` **页面/导航**:根页 = 已登录 ? ACT_HOME : ACT_LOGIN。栈:HOME → PLAYLIST → PLAYER;SETTINGS 任意页可进。 左缘左滑(`on_swipe LEFT`):PLAYER→pop 回 PLAYLIST;SETTINGS→pop;PLAYLIST→pop 回 HOME;HOME/LOGIN→`meow_ui_request_exit()` 退出 App。 ### 10.2 关键实现 **登录**(`ui/login.c`):进入即起后台 worker(网络全在独立线程,UI 不卡)。`ncm_login_unikey`(eapi) → `ncm_login_qr_url` + `ncm_login_qr_matrix`(qrcodegen) → 渲染 37×37 位图(白底+黑模块 6px,居中)→ 每 1.5s `ncm_login_poll`(801/802/803/800)。803:捕获 set-cookie 合并 → `ncm_ctx_set_cookie` 加密落盘 → `ncm_login_status_check` 取 uid/nickname → 置 done → UI tick `meow_view_page_replace(ACT_HOME)`。800(过期)自动刷新二维码;网络失败 tap 重试。 **主页**(`ui/home.c`):`meow_listview` 虚拟化(folder 图标 + 名字 + `N tracks` 副标题 + 右侧箭头);空态/会话过期/错误绘制物;topbar 标题=昵称 + 「↻」刷新;行数据轻量(不下载封面缩略图)。点击行 → `playlist_open(idx)`。 **歌单详情**(`ui/playlist.c`):`ncm_api_playlist_detail`(trackIds) + 分批 `ncm_api_song_detail`(≤256 首,NCM_SONG_MAX);行=歌名+歌手;点击行 → `player_play_song(row)`(起播 + 推播放页)。 **播放页**(`ui/player.c`):封面(`cover_request` 后台解码,`meow_ui_img_cover` 居中裁方 220px)+ 题/歌手 + 进度条(手绘,点 bar seek)+ 控制钮(prev/play-pause/next)+ 滚动歌词(`lyric_line_at(pos)` 居中高亮)。**横竖屏可切**:device LCD 为竖屏 480×720,故为**布局**切换(竖=封面上/歌词下;横=封面左/信息歌词右),topbar 「⟳」触发,`player_set_screen()`。进度/状态/歌词每 tick 读 `player_manager_*`(跨线程 mu)。 **设置**(`ui/settings.c`):3 行卡片(音质/缓存上限/退出登录)。音质→BottomSheet(Hi-res/Higher/Standard);缓存→BottomSheet(1/2/5/10/20/30/50/100 GB);退出→confirm 弹窗 → `appstate_logout()` + replace ACT_LOGIN。持久化 `MeowData/os.meow.ncm/data/` 下 `ncm.level`/`ncm.cache_limit_gb`(键与核心库 `ncm_cache_load_setting` 对齐)。 ### 10.3 播放链路(`app/play/player_manager.c`) ``` player_manager_play(idx) → 播放线程外层循环 → run_song(idx): ① song_local_file: ncm_cache_lookup 命中 → 直接播(离线);否则 ncm_api_song_url_auto(desired=设置音质, 逐档降级 hires→…→standard, 只收 mp3) + ncm_http_download 流式写缓存 temp → ncm_cache_commit(先落盘再播) ② mp3dec_ex_open(path, MP3D_SEEK_TO_BYTE) → audio_out_open(44.1k/16/2) ③ 循环 mp3dec_ex_read → audio_out_write(pcm, frames) → frames_done→pos_ms(~200ms 节流更新) 暂停=冻结(停读停写停推 pos,状态 PAUSED);seek=重开解码器+audio_out 并字节 offset 近似定位; 上下曲/停止=打断 run_song 回外层;播完=自动顺延下一首(无则 STOPPED) ``` - **缓存**:目录 `MeowData/os.meow.ncm/cache/`(`ncm_cache_*`),LRU 上限可配(默认 10GB,1~100)。 - **audio_out**:宿主/qemu 无 `/dev/snd/pcmC0D0p` → 回退 FILE WAV(`NCM_AUDIO_SINK=file` / `NCM_AUDIO_OUT=`)或 NULL(`NCM_AUDIO_SINK=null`,仅测 RT)。设备走 ALSA 内核 UAPI 直开(`sound/asound.h`,masks/intervals 重索引已按 musl 头修正),**真机时序待验证**。 ### 10.4 媒体会话 IPC(`app/play/ipc_media.c`,SDK 无媒体封装自写最小客户端) - 连接 `MEOW_IPC_SOCK`(缺省 `AF_UNIX /usr/data/meow_ipc.sock`)。 - `MSG_HELLO`(app_id=`os.meow.ncm`, flags=`MF_PLAYBACK`) → 读 ACK。 - 同 fd 每 ~1s 发 `MSG_META`(m_meta_t:title/artist/album/pos_ms/dur_ms/state=_1播 2暂停 0停 3缓冲,flags=MF_ART|MF_COVER(有 cover_current.jpg 时),artwork=该文件路径,app_id)。launcher 以最近活跃会话为准 → 通知中心媒体卡片 + Now Playing 页。 - `poll(fd,POLLIN,200ms)` 收 `MSG_PLAYCTL` → cmd 1播放/2暂停/3上一首/4下一首/5seek → `player_manager_*`。 - 无 launcher socket(宿主/qemu 无守护)→ log `ipc: no launcher socket` 并优雅返回。 ### 10.5 验证记录(宿主 + qemu-mipsel 双向实测) | 项 | 方式 | 结果 | |---|---|---| | 各页 UI | 宿主离屏 `MEOW_UI_DUMP` + PIL 转 PNG 目检 | login/home/playlist/player/lands/settings 全部正确(截图 `app/out/shots/*.png`) | | 登录真实二维码 | `NCM_PAGE=loginlive`:核心库真实 `ncm_login_unikey`→qr_matrix→渲染 | 真 unikey `40287b2e-9ef8-4f2e-8bf5-04febdaf631a`→37×37 可扫 QR(`out/shots/loginlive.png`) | | mipsel 二进制 | `qemu-mipsel ./ncm`(离屏+MEOW_UI_DUMP,timeout SIGTERM 干净退出) | 真实 mipsel 静态二进制跑通:`start`→`ipc: no launcher socket`(优雅)→登录页渲染**真实可扫 QR**(`out/shots/mipsel_qemu.png`) | | 解码链路 | `test_decode`(minimp3→audio_out FILE WAV) | MIPSTest.mp3 → 8.21s/48k/立体声/16bit PCM,WAV 合法(ch=2 rate=48000 frames=393984),**446× RT**(解码余量巨大) | | 构建 | `build_ncm.sh` mipsel | **0 warning**(自有代码 -Wall -Wextra);text 764KB | **宿主截图**:`app/out/shots/{login,loginlive,home,playlist,player,lands,settings,mipsel_qemu}.png`(PPM→PNG)。 注:宿主 5×7 点阵字仅 ASCII(中文/歌名在设备上以 gbk 大字库渲染;桌面截图为 ASCII label 布局自查)。 ### 10.6 md5 ``` 1e331964b62b6f5b0a593c5d97a4e030 ncm # mipsel 静态 App (app 源码 md5) 50dc58dc62a9d71a23e90bb00c79a36b app/app.c cf5e3283065dfce798d668c22ee87f65 app/ncm_app_internal.h b961bc769951208a09017f278c05a95b app/ui/login.c 5098bd843f6943bcda5aacfb7fae961f app/ui/home.c ef600c9d3a88eb2463d1c8f312a5a202 app/ui/playlist.c a55846822deb0246c6780d8a7ac99f46 app/ui/player.c 05fef4b6fa5692abf0da57db40ad6f90 app/ui/settings.c 013111dea3172fdeb5353a2ec69a71a5 app/ui/chrome.c 203a9932bde8a739b436e246ba634638 app/logic/appstate.c 48a92829ac414ffad5dbe27e0836c21b app/logic/lyric.c ed2d9658bc8707a5fd689b386da472f9 app/logic/cover.c e7cef918d2be04f6d4c5a25f62d2a9ab app/play/player_manager.c 4ceeb2f4f1b0a312c9468b9f77886dc7 app/play/audio_out.c 8c1716ca99cd773a0cf9a105f793b845 app/play/ipc_media.c 6ca4b2137c4a424d823f94a8b3fcd037 app/play/minimp3_impl.c a2ca48b2c6afa6c36f045ba8b2687fc5 app/test/test_ncm_app.c 9ebfb5401c7818cfd0c21d640b07c521 app/test/test_decode.c ``` ### 10.7 出入清单 ``` 新增(仅 patch_v2/ncm_app/): app.json # 应用描述 build_ncm.sh / build_ncm_host.sh # mipsel / 宿主构建 app/app.c ncm_app_internal.h app/ui/{chrome,login,home,playlist,player,settings}.c app/logic/{appstate,lyric,cover}.c app/play/{player_manager,audio_out,ipc_media,minimp3_impl}.c app/test/{test_ncm_app,test_decode}.c out/shots/*.png # 验证截图 修改: REPORT.md # 追加本批记录 (未动 sdk/、meow_launcherd/、vendor/、core/;核心库 §9 原样使用) ``` ### 10.8 真机待验证项 1. **ALSA 直开 `hw:0,0`**:本层用内核 UAPI(`/dev/snd/pcmC0D0p` + ioctl hw_params:RW_Interleaved/S16_LE/44100/2ch)——**设备时序/权限/采样率匹配**待真机验证;宿主已回退 FILE sink 证解码链路。 2. **真实扫码登录(803)**:set-cookie 捕获→加密落盘→`login_status` 校验链路按文档实现但**未真扫**;建议真机扫码验证一次 `MUSIC_U` 内容与 home 拉歌单闭环。 3. **媒体会话**:需 launcher 在真机跑(本 App 首真实媒体源);验证通知中心媒体卡片/Now Playing 显示 MSG_META + 下发 MSG_PLAYCTL 生效(本 App 已记 `ipc: no launcher socket` 为优雅降级,真机有 socket 即上报)。 4. **大字库**:app.json `fonts:gbk` 声明已加;真机验证歌名/歌词中文渲染(宿主 ASCII 5×7 不含中文)。 ### 10.9 诚实出入 / 已知限制 - **封面为歌单封面**(`playlist_detail` 的 coverImgUrl)而非**逐歌曲专辑封面**:`ncm_song_t` 未含 `al.picUrl`,为不改 §9 核心库(已驯化契约)故 v1 用歌单封面代为 artwork;逐曲封面需给 `ncm_song_t` 增 `al.picUrl` 字段(附加、向后兼容,建议二期)。 - **歌词为同步加载**:播放页 enter 时 `player_manager_load_lyric` 同步网络拉取(UI 短暂阻塞);建议后续改后台线程+就绪标志(音频线程独立不受影响)。 - **歌单曲目截断**:`NCM_SONG_MAX=256`(大歌单显示前 256 首);歌单数 `NCM_PLAYLIST_MAX=120`。 - **launcher 媒体卡片封面**:经 MSG_META `artwork` 传 `/cover_current.jpg` 路径;若 launcher 读不到该路径则卡片仅题/艺/进度(App 内封面始终有)。 - **IPC 仅处理 MSG_META/MSG_PLAYCTL**(未接 MSG_SYS,媒体会话最小闭环,符合 v1)。 - **bss≈924KB / text≈764KB**:mipsel 静态;14MB 内存富余,但 bss 含封面下载缓冲(288KB)+QR 矩阵(31KB)+mbedtls 静态,已留充足余量。 - **`meow_snack`/页脚**为 App 自绘(非 SDK 全局通知,避免与 launcher 系统 UI 冲突)。 --- ### 10.10 真机 weapi 挂死根因定位 + 修复(2026-08-31,已真机闭环) > 现象:真机 `music.163.com/weapi/...`(`login_status_check`/`user_playlist`)请求永久卡死, > 线程 `wchan=poll_schedule_timeout`,>10 分钟不超时;同机的 eapi(`interface.music.163.com`)正常。 > 定为 **多个独立缺陷叠加**,其中「超时用墙钟算」是那把导致「>10 分钟不超时」的关键一把。 #### 根因(按“致卡死”权重排序) 1. **超时/截止用 CLOCK_REALTIME(墙钟,非单调)—— 这是「>10 分钟不超时」的真根因。** `ncm_http.c` 的 `wait_io()` 与 `deadline` 都用 `ncm_now_ms()`(`CLOCK_REALTIME`)。设备在请求进行中被 NTP 把墙钟**拨回**了 Δ(校时 step,方向可正可负)。于是 `rem = (uint32_t)(deadline - now) = (now0 + tmo) - (now0 - Δ) = tmo + Δ`;若 Δ 为小时级/天数级, `rem` 变成巨大的 uint32_t(甚至溢出回绕),`mbedtls_net_poll()` 因此拿到远大于 15s 的等待窗口, 线程长期阻塞在 `poll_schedule_timeout`。墙钟拨回越大,阻塞越久——这正解释了“>10 分钟且与 eapi 行为无关”(eapi 恰好在该时刻没撞上拨回,或撞上后被验证时序放行)。 **宿主演示**(`bench/monotonic_test.c`):拨回 6 小时。 - 墙钟 `rem = 21615000 ms ≈ 6.0 h`(poll 阻塞 6 小时) - 单调钟 `rem = 15000 ms = 15 s`(不变) > 结论:**超时/截止计算必须用 `CLOCK_MONOTONIC`**(永不跳变)。墙钟只留给 request_id/_ntes_nnid/ > 落盘时间戳这类“取当前墙钟”的场合。 2. **响应头解析“隔行跳行”,漏掉 `Content-Length`。** `parse_headers()` 旧循环 `while((p=strstr(p,"\r\n"))){ p+=2; ...; p=eol+2; }` 每轮把**下一行**吞掉: 处理 `Server` 后直接跳到 `Connection`,中间 `Date`/`Content-Length`/`Vary`… 被跳过。 导致 `content_length` 停留在 `SIZE_MAX` → `body_st_init` 落入 `BS_CLOSE`(读到对端关闭)。 若服务器 keep-alive + 无 chunked(网易常见),body 将一直读到对端关;配合根因①的墙钟异常, 阻塞被拉长成“永久卡死”。宿主复现:`/-11 FULL`(见根因③)前先看到 `status=0 content_length=0`。 修法:**逐行遍历**(先定位状态行 `\r\n`,再逐 header 行到空行),并顺带修正状态码解析(旧代码取 `"1.1 200 OK"` 的首空后即报 `200`,实际取到 `1.1` → `status` 恒 0)。 3. **请求头缓冲 1024 太窄 → 真机大 cookie 时 `NCM_ERR_FULL`,请求根本没发出。** weapi 带真实会话 cookie 时 `extra_hdrs`(UA + Content-Type + Cookie + Referer)可达 ~1.1KB, 旧 `char headbuf[1024]` 溢出,`http_run` 返回 `-11 FULL`(真机实测 `dev_repro2`:`rc=-11`, `status=0 content_length=0 headers_len=0`,即请求未发)。修法:`headbuf[8192]`(≥ extra_hdrs + ~200)。 4. **weapi 发送路径把长 cookie 值截断(MUSIC_U 只剩 191 字节)→ 服务端识别为游客(profile:null)。** `ncm_req.c` cookie map 的 `ck_t.val[192]` 会把 834 字节的 `MUSIC_U` 截到 191 → 服务端看成 guest 会话, `login_status` 返回 `{"code":200,"account":null,"profile":null}`(`-13 AUTH`)。修法:cookie map 改存 **值指针 + 长度**(`(const char*)val, size_t vlen`,不复制不截断),并按名去重;生成值(_ntes_nnid/ WNMCID/NMTID)buffer 提升到函数作用域保证生命周期。`cookie_full` 4KB。 5. **响应头/体切分顺序 bug(我修 `split` 时引入后已修正)**:在 `split` 处 NUL 终结头部会覆盖 body 首字节, 导致 body 首字节变 NUL → JSON 解析失败(`-9`)。已改为**先送 body 再 NUL 终结头部**(二者同址)。 6. **`Content-Length: 0` 没立即结束**:补 `body_st_init` 对 `content_length==0` 直接置 `BS_DONE`,避免等对端关。 #### 修法(全部在 `patch_v2/ncm_app/`) - `core/ncm_common.h/.c`:新增 `ncm_monotonic_ms()`(CLOCK_MONOTONIC);`ncm_now_ms` 注明仅用于墙钟。 - `core/ncm_http.c`: - `wait_io` + `deadline` 改用 `ncm_monotonic_ms()`; - 重写 `parse_headers`(逐行、状态码用 `strtol` 忽略 reason、去掉 16KB 栈缓冲改 128B); - `headbuf[1024]->[8192]`;`split` 处先送 body 再 NUL 终结;`Content-Length:0` 直接 `BS_DONE`。 - `core/ncm_req.c`:cookie map 改 `(ptr,len)` 存储(不截断、按名去重);生成 cookie 值 buffer 提作用域; `cookie_full[2048]->[4096]`。 - `core/ncm_login.c`:`login_status_check` 设 `timeout_ms=6000`(硬上限)且 `respbuf[16384]`(容纳真实 profile)。 - `app/ui/login.c`:803 后即时 `status_check` 取 uid/nickname 并 `appstate_set_logged_in` 落盘 (修复“803 后 uid 恒空”;worker 线程执行,不卡 UI)。 - `app/logic/appstate.c`:`appstate_load_playlists` 记 `home: playlists rc=? count=? uid=?`(供真机日志核对; `appstate_ensure_uid` 因 uid 已落盘而短路,不再阻塞歌单页;status_check 有 6s 硬上限兜底)。 #### 验证记录(宿主 + 真机) **宿主(`./test/test_ncm_core_host`,全 PASS;另 benchmark 复现工具)** - `bench/monotonic_test.c`:墙钟拨回 6h → `rem=21615000ms`;单调钟 → `rem=15000ms`。**根因①钉死**。 - `bench/host_repro.c`(weapi account/get,空 cookie):修复后 `rc=0 status=200 content_length=42 body_len=42`, body 正确为 `{"code":200,"account":null,"profile":null}`(修复前 `status=0 content_length=0 body=0`)。 - `bench/host_fullcookie.c` + 真机解密 cookie:全量 cookie(未截断、去重)→ 服务端返回 `body_len=1161`(真实 profile); 被截断/带旧重复 token 的 cookie → 服务端返回 `body_len=42`(guest)。**根因④钉死**:去重+全量才被认可。 - 宿主测试套件:cache/qrcodegen/network(unikey/poll)/no-credential 各用例全 PASS(无回归)。 **真机(最终确认 1 次,风控:真实 cookie 请求严格受限)** - `bench/verify_home.c`(复刻 App home 链路:status_check→user_playlist,链接修复后 core): ``` status_check rc=0 elapsed_ms=721 uid=408996274 nick=Any无言 user_playlist rc=0 elapsed_ms=2562 count=64 [0] Any无言喜欢的音乐 (5191 tracks) [1] Docu (1 tracks) ... [7] Ethereal (2 tracks) VERIFY OK: user=408996274 playlists=64 ``` 即 **home 真实列出用户 64 个歌单**,满足验收。 - 真机中途定位记录(修复前):`dev_repro2` 见 `rc=-11(全量时 status=0/content_length=0)`(根因③);`dev_repro` 见 `cookie_len=1842`(重复 cookie,根因④由 host_fullcookie 证明需去重+全量);修复后 `dev_repro` 见 `status_check rc=0 uid=408996274 nick=Any无言`(根因②③④⑤全消)。 - 部署:`/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm`(先 `ncm.new` 再 `mv`),md5 `bf6d2a79504d49cdc9b87e6f34cc6aec`。 #### 出入 / 已知限制(本批) - **mipsel `-Wall -Wextra` 自有代码 0 warning**(`build_ncm.sh` 复核);vendor 豁免。 - **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机;未动设备系统/恢复区。** - **历史遗留:真机 `session.enc` 里是旧版重复 cookie(MUSIC_R_T/A_T 等重复多次)**;本次通过发送路径 “按名去重 + 全量不截断”规避(`verify_home` 已证明服务端认可)。未追改 `merge_cookie`/落盘去重 (发送端已去重,功能不受影响);如需落盘也干净,可后续在 `ncm_login_poll` 的 `merge_cookie` 处按名去重。 - **`appstate_ensure_uid` 仍同步跑在 UI 线程**,首次(uid 未落盘时)会占 UI 至多 ~6s 硬上限(正常 ~0.7s); 真正的“不阻塞歌单页”可由「uid 落盘后短路」+「6s 硬上限」保证,未改为后台线程(避免范围扩散)。 --- ### 10.11 真机三问题修复:启动卡顿 + 大歌单 Load Failed + 小歌单 No tracks(2026-08-31,已真机闭环) > 现象(用户真机实测):登录通、歌单列表 64 个。三个问题: > ① 有登录态点开 App 卡数秒才进;②「我喜欢的音乐」(5191 首) 点开 Load Failed;③ 其余歌单全部 No tracks。 > 本批定位多处独立缺陷并修复,全部经宿主复现 + 真机验证。仅改动 `patch_v2/ncm_app/`。 #### 根因(按现象对应) - **P1 启动卡数秒**:`app/ui/home.c` `home_page_enter → home_reload → appstate_load_playlists`(weapi user/playlist)在 **UI 线程同步执行**;真机该 weapi 往返 ~2.5s+(verify_home: `elapsed_ms=2562`),uid 未落盘时还叠 status_check(6s 硬上限)。期间 UI 冻死(`paint_full` 不执行)。 - **P2 大歌单 Load Failed**:`core/ncm_api.c:ncm_api_playlist_detail` 发 `{"id":..,"n":100000,"s":8}` + 响应 cap `NCM_RESP_MAX(1MB)`。5191 曲歌单 detail 真机实测 `status=200 cl=3168819`(3.0MB:`playlist.tracks` 塞满歌曲对象),1MB cap 溢出 → `NCM_ERR_FULL(-11)` → Load Failed。关键:**`trackIds` 数组与 `n` 无关**(n=1 时仍完整返回 5191 条,实测),是 `tracks`(完整歌曲对象)撑大响应。 - **P3 小歌单 No tracks**:`ncm_api_song_detail` 把曲目 id 数组塞进 `{"c":"[...]"}` 时**内部引号未转义**,实发 `{"c":"[{"id":2048962657}]"}`(坏 JSON),服务端回 `200 Content-Length:0` → jsmn 空体 → `NCM_ERR_JSON(-9)` → 0 首。宿主复刻:转义后 `{"c":"[{\"id\":2048962657}]"}` → 服务端回 `2146B` 正常 songs。 - **P0(连带新发现)**:网易云歌曲 id 可 >2^31(如 `2746301825`),而 App 用 `long` 存 song_ids;**MIPS32 的 long=32 位**,`strtol` 溢出饱和到 `LONG_MAX(2147483647)` → 大歌单 trackIds 全错(`2746301825` 被解析成 `2147483647`),song_detail 拿错误 id 只回 30 首。宿主 aarch64(long=64 位)不暴露,真机 MIPS32 才炸。 - **P3b(连带)**:批量 song_detail 转义后 json 变长 → eapi `params[2048]` 溢出 → `ncm_crypto_eapi` 返 `NCM_ERR_CRYPTO(-8)`(batch=64 实测 rc=-8)。 #### 修法(全部在 `patch_v2/ncm_app/`) - **P1** `app/ui/home.c`:加载改后台 worker(`home_worker`,detached);`home_reload` 立即返回并画 LOADING;`home_tick` 按 `g_home_gen` 完成代数回填。`app_on_tick` 路由 `home_tick`。ensure_uid(status_check)+ user_playlist 全在 worker,UI 不阻塞。(顺带消掉 §10.10 遗留的「ensure_uid 同步跑 UI 线程」。) - **P2** `core/ncm_api.c:ncm_api_playlist_detail`:`n` 100000→**1**(`tracks` 只带 1 首,3MB→~900KB);响应 cap 1MB→**2MB**(trackIds 大的歌单留余量)。 - **P3** `core/ncm_api.c:ncm_api_song_detail`:`c` 数组内部引号转义(`{\\\"id\\\":%lld}`)。 - **P0** `core/ncm_common.{h,c}` 新增 `ncm_parse_i64`(strtoll);`core/ncm_json.{h,c}` 新增 `ncm_json_get_i64_at`;`core/ncm_api.h` 两处 id 签名 `long*`→`int64_t*`;`app/ncm_app_internal.h` `song_ids[]` long→int64_t。 - **P3b** `core/ncm_req.c` eapi 局部缓冲 `full2[3072]→[4096]`、`params[2048]→[4096]`。 - **防大歌单冻 UI**:`app/ui/playlist.c` 歌单详情同样后台化(`playlist_worker` + `playlist_tick`);`appstate_load_playlist_songs` 不再覆写 `g_st.active_playlist`(防后台 worker 覆盖用户已切换的歌单)。 #### 验证记录 **宿主(同 core 库 + 真实服务器)** - `bench/host_chain_probe`(detail→song_detail 背靠背,小歌单 `18206253175`):修前 `song_detail rc=-9`;修后 `rc=0 bn=1 song_count=1`。 - `bench/host_big`(大歌单 `581857874`):修前 big detail 溢出;修后 `detail rc=0 idn=256` + 4×64 批全 `rc=0` → `song_count=256`,首曲 `Chrono Rewind / CVRCH`。 - 宿主 eapi 复刻:未转义 c → 服务端 `200 CL:0`;转义后 `200 len=2146`。**P3 钉死**。 - `test/test_ncm_core_host` 全 PASS;`build_ncm.sh` mipsel `-Wall -Wextra` 0 warning。 **真机(`bench/verify_fixes`,一次运行,小歌单 2 请求 + 大歌单 5 请求)** ``` device_id=BD6DE5CC2DBCB617253A00438897DC6C02FFC7CFDB99DB0FA08E cookie_len=1842 [small(Docu/1)] detail rc=0 idn=1 ids[0]=2048962657 song off=0 batch=1 rc=0 bn=1 RESULT song_count=1 [0] Verve (feat. FJØRA) / Ryan Taubert [big(5191)] detail rc=0 idn=256 ids[0]=2746301825 ← 修 P0 后正确(修前=2147483647) song off=0/64/128/192 batch=64 全 rc=0 bn=64 RESULT song_count=256 [0] Chrono Rewind (时空回溯) / CVRCH ``` → **P2/P3/P0/P3b 真机全消**:小歌单 1 首、大歌单 256 首(不再 Load Failed / No tracks)。 **真机启动(部署后后台运行 5s)** ``` 6213257 [os.meow.ncm] INFO start 6217019 [os.meow.ncm] INFO home: playlists rc=0 count=120 uid=408996274 ← 后台 worker 完成 6218319 [os.meow.ncm] INFO exit ``` → 启动即进 home;`home: playlists` 由 worker 与 UI 并发产出,UI 不再被 weapi 阻塞(**P1**)。修前旧二进制日志 `start→home playlists` 同链路为 UI 线程同步(`home.c` 阻塞)。 **MIPS32 溢出佐证(P0)**:修 P0 前真机 `verify_fixes`:`big detail idn=256 ids[0]=2147483647`(strtol 溢出饱和);修后 `ids[0]=2746301825`。 #### 部署 + 出入清单(本批,只改 `patch_v2/ncm_app/`) - 部署:`/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm`(先 `ncm.new` 再 `mv`),**新 md5 `f6bd765a374effaf866784c3165e476e`**(旧 `bf6d2a79504d49cdc9b87e6f34cc6aec`)。 ``` 修改: core/ncm_api.c # P2 n=1+2MB cap;P3 c 转义;P0 int64 签名 core/ncm_api.h # int64_t ids 签名 core/ncm_req.c # P3b eapi 局部缓冲提升 core/ncm_common.h/.c # 新增 ncm_parse_i64 core/ncm_json.h/.c # 新增 ncm_json_get_i64_at app/app.c # app_on_tick 路由 home_tick/playlist_tick app/ncm_app_internal.h # song_ids→int64_t;声明 home_tick/playlist_tick app/ui/home.c # P1 home 加载后台化 app/ui/playlist.c # 歌单详情后台化(防大歌单冻 UI) app/logic/appstate.c # load_playlist_songs 不覆写 active_playlist 新增: bench/verify_fixes.c # 真机复验小歌单+大歌单链路(回归工具) REPORT.md # 本批记录 (未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区) ``` 变更文件 md5: ``` ceb89ee9e83752208056e2eadb5330fd core/ncm_api.c 63d4af4307c5cdb3922e59dc70116407 core/ncm_api.h cc1ab202c8d603e296f02715718a8a25 core/ncm_req.c 45efe7071b8cbff552ac89fe8b2e9495 core/ncm_common.c 8fc658f656a1004f80ab5bc1555c2048 core/ncm_common.h df46e0cd3215d359025519b4d41316d6 core/ncm_json.c 6d473afe13176230268c68ddc655cb34 core/ncm_json.h 3d2daea41204c887915d8c078543afe0 app/app.c 282c24806fe0e9c97adf464f6a5c2776 app/ncm_app_internal.h 10c3b4b1097f1b0d82550659d0a87c4f app/ui/home.c 0efef753b32eef144f2c30e4592fbfea app/ui/playlist.c 94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c bcfd75e5eb872a751c166c456eea0184 bench/verify_fixes.c f6bd765a374effaf866784c3165e476e ncm ``` #### 诚实出入 / 已知限制 - **大歌单 load 慢(真机 detail ≈30s)**:`/v6/playlist/detail` 无论 `n`/`s` 都返回**全量 trackIds**(5191 个 ≈900KB,实测 n=1/0/100000 均 5191 条),无法用该端点分页/限流。真机 MIPS32 下载 + jsmn 解析 ≈30s(宿主 4.7s,~7× 慢,符合 MIPS vs aarch64 预期)。已用「playlist 后台化」避免冻 UI(显示 LOADING),但大歌单首屏仍 30s 级等待;要根治需改走 `/api/playlist/track/all`(服务端分页)或流式 jsmn——**建议二期**。 - **`n` 只限 `tracks` 不影响 trackIds**:`n=1` 对 ≤256 曲歌单仍完整返回全部 trackIds(idn=实际曲数,无回归);仅把「多出的完整歌曲对象」从响应里去掉,从而把 3MB 压回 ~900KB。 - **曲目截断仍 `NCM_SONG_MAX=256`**(现协议):大歌单显示前 256 首,未做滚动加载(二期)。 - **`g_st.songs[]/song_count` 由 playlist worker 写、play/IPC 线程读(§10.9 FM4 旧已知风险)**:入播放页仍在同歌单,竞态窗口极小;沿用未加锁(与项目现状一致)。 - **真机真实请求计数**:本批新增 ~20 次(detail_probe 抓样 1 次(5 请求,样本落盘)+ song_probe 1 + app_path_probe 2 + verify_fixes 2 次 11 + 启动验证 2)。已尽量「一次请求存样本、离线重放」,但修复前后复验需真实响应,属必要开销。 - **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区。** --- ## 11. 网易云 UI 重做(按 NCM_UI_DESIGN.md,2026-08-31)—— 双套配色 / 黑胶播放页 / 质感 > 依据 `devdocs/plan/NCM_UI_DESIGN.md` 将 App 从「文件管理器列表风」重做为**网易云风格**: > 白红/黑红双套配色、主页双页签「我创建的/我收藏的」、独立搜索页、歌单详情封面头块+「播放全部」、 > 播放页黑胶唱盘(fill_rect 行扫构造圆 + 方盖内切裁圆封面 + 唱针)。仅改动 `patch_v2/ncm_app/`。 > 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机。 ### 11.1 双套配色机制(§3.1/§3.3) - **新增 `app/ui/ncm_colors.c`**:`ncm_pal_t{ncm_palette(dark)}` 两套 App 调色板(白红 / 黑红 + 播放页/登录专属色)+ `g_dark` 全局 + `ncm_on_theme(mode)` 主题侦听。 - 品牌红走 App 侧(SDK 全局 primary 是 MD3 蓝,App 无权改——红只落 App 自绘层)。 - 中性色(surface/onSurface/onSurfaceVariant/outlineVariant/error)一律 `meow_theme_get()`; **全程语义色 + App 调色板,零硬编码散落**(§3.1 契约)。 - **`app/app.c`**:`meow_theme_add_listener(ncm_on_theme)`(旧代码传 NULL 无效)+ 启动同步 `g_dark = (meow_theme_get_mode()==MEOW_THEME_DARK)`。系统明暗切换 → `ncm_on_theme` → 整 App 重绘实时换肤。 - 色值(§3.2):day red `#C20C0C` / red_lit `#EC4141` / red_ctr `#FBE9E9`; night red `#EC4141` / red_lit `#FF6A6A` / red_ctr `#3A1919`;登录 hero night `#7A0E0E`; 播放页底 day `#FFFDF9` / night `#0C0D10`;盘体 day `#1A1A1A` / night `#141414`。 验证(宿主抽样像素):day 组头红条 `(198,12,8)`≈`#C20C0C`,night `(239,65,66)`≈`#EC4141`。✅ ### 11.2 数据层(§7.0 明示缺口) - `core/ncm_api.h`:`ncm_playlist_t` 增 **`int is_collected;`**(1=收藏的/订阅他人,0=自己创建)。 - `core/ncm_api.c`:`ncm_api_user_playlist` 解析响应 **`subscribed`**(`obj_bool`)写入 `is_collected`, 支撑「我创建的/我收藏的」分组(原实现只取 id/name/coverImgUrl/trackCount)。 - `core/ncm_api.h/.c`:新增 **`ncm_api_cloudsearch`**(weapi `/api/cloudsearch/pc`,type=1 单曲), 解析 `result.songs[]`(id 用 int64/name/ar[0]/al/dt),关键词 `"` `\` 转义防坏 JSON。 ### 11.3 页面改动 - **登录页**(`ui/login.c`):品牌红 hero 带(y=0 起,沉浸式)+ 红点 logo + 「网易云音乐/扫码登录」 标题区 + QR 卡(圆角 16 白底/surfaceContainer + 内描边)+ 状态字(day red/night red_lit)+ 底部提示。 - **主页**(`ui/home.c`):红系 topbar(标题「我的音乐」+ 刷新/设置/搜索动作,搜索按钮最右)+ **个人区卡片**(首字圆形头像 red_ctr + 昵称 + ID + 箭头)+ **双页签「我创建的/我收藏的」** (`meow_tabbar`,红下划线/红选中字叠层注入)+ **组头红条**(red_ctr 底 + 红字 + 红侧条)+ 歌单列表(rounded card 行 + 红 mini-vinyl 缩略 + 红色按压竖条);列表数据源按 `is_collected` 过滤分组。 - `MEOW_LIST_SEL_NONE` + 自定义红态叠层(§4.2:SDK listview MD3 蓝选中不可改,改叠红条)。 - **保留 home_worker 后台异步加载**(§10.11 P1 修复),未回退成同步。 - **搜索页**(`ui/search.c` 新增,`ACT_SEARCH`):TextField(SDK IME,仅英文/数字)+ **红色「搜索」确认钮** + 结果列表(点击可播,复用 `g_st.songs[]` 作为当前播放上下文)。**风控纪律**:搜索只在用户点「搜索」钮时发 (进入页面不自动搜索、不逐键发请求);网络走后台 worker(search_worker)。 - **歌单详情**(`ui/playlist.c`):封面头块(圆角封面/占位 + 歌单名 + 「创建者 · N 首」)+ 操作条 (**红色「播放全部」** FILLED + 「收藏」OUTLINED(v1 置灰→snack) + 「评论」红字+红badge(v1 置灰→snack))+ 曲目列表(歌名 + 歌手·专辑 副标题);保留了 §10.11 的后台 `playlist_worker`。 - **播放页**(`ui/player.c`):**黑胶唱盘**(`fill_rect` 行扫构造圆盘 + 环槽减淡 + 方盖内切圆 + **四角把「圆外-方外」盖回盘色 → 封面成圆**,§5.1 技巧)+ **唱针**(3px 唱臂 + 端头, 播放压盘/暂停微抬 6px)+ 高光弧 + 进度条(已播红)+ prev/play/next 控制钮(play 红)+ 滚动歌词 (当前行红高亮居中,裁剪到歌词带)+ 竖/横布局可切(沿用 player_set_screen)。 - **恒沉浸**:播放页底 day 瓷白 `#FFFDF9` / night 近黑 `#0C0D10`,不随系统明暗整体反转;accent (进度红/play 钮/歌词高亮)日夜换红明暗(day `#C20C0C` / night `#EC4141`,§5.4)。 - **设置页**(`ui/settings.c`):4 行卡片(音质/缓存上限/退出登录/关于)+ 分组说明;退出登录用 error 色; 音质/缓存 → BottomSheet;退出 → confirm;关于 → snack。行圆角 12,语义色。 - **chrome.c**:`ncm_draw_empty` kind 大字改品牌红;页脚 dim。 ### 11.4 性能(§6 专项) - **播放页分带刷新**:内容拆「盘带 `g_disc_r`」+「动态带 `g_dyn_r`」两个绘制物,bbox 互不重叠。 `player_tick` **只标脏变化的那一条带**——进度/歌词/时间只刷动态带,盘带仅 歌曲/封面/播停态 变化才重画 (修原 `player_tick` 每 tick 刷整 `r_cont` 的隐患,§6.1/§6.2)。 - 黑胶圆盘仅 封面/状态变化 时重画;歌词行裁剪到歌词带(自定义绘制物不按 bbox 裁剪,防溢出叠控制钮)。 - 封面限幅沿用(`NCM_COVER_MAX=288KB`,单张);搜索结果上限 `NCM_SEARCH_MAX=32`(无大缓冲,§6.7)。 ### 11.5 验证记录(宿主 + mipsel) - **宿主离屏 `MEOW_UI_DUMP` 双套配色各截一轮**:7 页 × 2 主题 = 14 张 `app/out/shots/{login,home,playlist,player,lands,settings,search}_{dark,light}.png`(PPM→PNG)。 - 红色抽样:day `#C20C0C`(组头红条)、night `#EC4141` 均正确;播放页恒沉浸(瓷白/近黑)✅。 - 黑胶盘/唱针/封面裁圆/进度/控制/歌词居中 目检正确 ✅;主页双页签、个人卡、组头红条、红缩略 ✅。 - **`test/test_ncm_core_host` 全 PASS**(无回归);**`out/test_ncm_core_mipsel`(qemu)全 PASS**。 - **mipsel `build_ncm.sh` 自有代码 `-Wall -Wextra` 0 warning**;`ncm` 静态,text 803084 / data 824 / bss 928356。 - **qemu-mipsel 冒烟** `timeout 10 qemu-mipsel ./ncm`:`start`→`ipc: no launcher socket`(优雅)→登录页 unikey 重试(rc=-5 无 CA,预期)→`exit` 干净退出,无崩溃。 - 未做(诚实):真机搜索/歌单请求仅留最少验证或不打(风控约束);未真机扫码/播放出声(用户联调项)。 - **真机部署未执行**:本机无可达设备主机(`meowos.local` 不解析;ssh 无设备别名/IP,WORK_STATE 亦标注 热点组播不通)。二进制已就绪(md5 `90a25cf0c193fd965d6685fb099bacb7`),部署命令留用户执行: `scp ncm root@:/usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new && ssh root@ 'mv /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm.new /usr/data/mnt/sd_0/MeowAPPs/os.meow.ncm/ncm'`(先 .new 再 mv)。 ### 11.6 md5(自有源码 + 二进制) ``` 90a25cf0c193fd965d6685fb099bacb7 ncm # mipsel 静态 App(新) c2921983e878ce0cefe1f279f6d16d3f app/app.c 8585677b732420ead899c34d07823a05 app/ncm_app_internal.h 359ad93833d3ef1914c97da28d249d09 app/ui/login.c 970e4ec732036ae8d8de137c960863c4 app/ui/home.c ac9e1585e96b1a1a336cbbc9a82a8c86 app/ui/playlist.c 3023fb024ea096e458b52fb4255db709 app/ui/player.c c4f6cd8569d6ddc4dd15d4111752ed77 app/ui/settings.c ba78835b3fbe6166bbe39f2e9ea9041a app/ui/search.c 54f10365ed640130ee33fba2e674b547 app/ui/ncm_colors.c aa93822cd735f3912556df6a301a3580 app/ui/chrome.c 94d10237c1aa73f7273f82ff319792f5 app/logic/appstate.c 539252a2ea6a6a675f94356898176b99 core/ncm_api.h 1c1c05b2fada19c40e8a83a2742afbd7 core/ncm_api.c 30ab44e6f6cbc8b07eb477e7b8ee82a3 build_ncm.sh 71ed280698b67cba76a90d19a43f8458 build_ncm_host.sh 2a9663abe58609fafcc22949b8e8771c app/test/test_ncm_app.c ``` > 注:`app/logic/appstate.c` 本批未改(md5 未变 `94d10237...`);`build_ncm_host.sh` 因补链 SDK > `meow_ui_nav/meow_ui_media/meow_notify_ipc` 而更新(宿主 SDK 源后来新增这些文件,原脚本缺链导致 > `meow_notify_ipc_notif_open` 未定义;属构建脚本适配,非 SDK 改动)。 ### 11.7 出入清单(只改 `patch_v2/ncm_app/`) ``` 新增: app/ui/ncm_colors.c # ncm_palette/g_dark/ncm_on_theme/ncm_fill_round/ncm_draw_button/first_glyph/ncm_text_fit app/ui/search.c # 搜索页(IME + cloudsearch + 结果列表) 修改: app/ncm_app_internal.h # ncm_pal_t/g_dark/ACT_SEARCH/搜索状态/新页声明/调色板助手声明 app/app.c # 主题侦听 + search 页建/路由 app/ui/{login,home,playlist,player,settings,chrome}.c # 重做/重着色 core/ncm_api.h/.c # is_collected(§7.0) + ncm_api_cloudsearch app/test/test_ncm_app.c # is_collected mock + NCM_THEME 双主题 + search mock build_ncm.sh / build_ncm_host.sh# 源列表加 search/ncm_colors;host 补链 SDK nav/media/notify_ipc (核心库 §9 其余、vendor/、sdk/、meow_launcherd/ 未动;未 commit/push/刷机) ``` --- ## 12. 二期四件功能批:专辑封面 / 歌词后台加载 / 大歌单分页 / UI 遗留修复(2026-08-31) > 依据任务批(REPORT §9-11 之上)做四项:① `ncm_song_t` 加 `al.picUrl`,播放页黑胶封面用真实专辑图 > (沿用 `cover.c` 缓存/限幅);② 播放页歌词改后台 worker + 就绪标志,enter 不再同步阻塞拉词; > ③ 大歌单 `playlist/detail` 全量 trackIds 太大(5191 首 ≈900KB,§10.11 已记录)→ 改走 > `/api/playlist/track/all`(eapi 服务端分页),曲目列表滚动加载(50 首/页);④ 修 UI 遗留: > 歌词字面 `\n` 未转换 + 播放页底部提示豆腐块(字库缺字)。 > 仅改动 `patch_v2/ncm_app/`;未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备。 ### 12.1 专辑封面(`ncm_song_t.al_pic` + 播放页用真实专辑图) - `core/ncm_api.h`:`ncm_song_t` 增 **`char al_pic[512]`**(`al.picUrl`,追加字段向后兼容)。 - `core/ncm_api.c`:抽出 `parse_songs_array()` 统一解析歌曲对象数组(`id/name/ar[0].name/al.name/al.picUrl/dt/fee`, id 数字/字符串均以文本拷贝,兼容 >2^31)。`ncm_api_song_detail`、`ncm_api_cloudsearch` 改共用该函数 → **歌单/搜索两路都带上专辑封面**。 - `app/ui/player.c` `player_play_song`:黑胶封面 URL 改为 `s->al_pic[0] ? s->al_pic : g_st.cover_url`(无 al_pic 回落歌单封面); `player_tick` 在 `sc != g_last_song`(切歌/自动顺延)时对新曲 `cover_request()` 换封面。 - 封面缓存/限幅**沿用** `app/logic/cover.c`(≤`NCM_COVER_MAX` 288KB、单张、后台线程下载解码 + `cover_current.jpg` 供 launcher),未改。 ### 12.2 歌词后台加载(`player_manager.c` + `player.c`) - 新增**持久歌词 worker 线程**(`lyric_worker`,detached):每 300ms 读 `g_play.cur_song`,曲目 id 变化即 `lyric_clear()` → `ncm_api_lyric` → `lyric_parse()`(网络+解析全在后台);状态 `g_lyr_state`(0 空闲/1 加载中/2 已拉完)。 - `player_manager_load_lyric()` 改为**非阻塞触发**:只 `lyric_ensure_worker()` 启动 worker 即返回(UI 不再同步阻塞)。 换曲/自动顺延由 worker 轮询自捕获,无需逐处触发。 - 新增 **`int lyric_state(void)`**;`player.c` `plr_draw_dyn` 歌词空态改为:`lyric_state()==2 ? "no lyric" : "loading lyric..."` (消除“无词”与“加载中”歧义)。 - 竞态(§10.9 FM4 已接受)沿用:UI 读 lyric 模块与 worker 写为良性(数组定长、越界已防护,最坏一帧旧/空行自愈)。 ### 12.3 大歌单分页(`/api/playlist/track/all` 服务端分页 + 触底加载) - `core/ncm_api.h/.c`:新增 **`ncm_api_playlist_track_all(device_id, cookie, pid, offset, limit, songs, max, &n, &total)`** → eapi `POST /api/playlist/track/all`,body `{"id","offset","limit","n"=limit,"s":8}`,直返该页歌曲对象(含 `al.picUrl`, **免去 detail 瓶颈 + 二次 song_detail**)。数组路径防御 `songs→playlist.tracks→data.songs`;`total` 取 `total→count→playlist.trackCount→musicCount`。 另导出 **`ncm_api_parse_track_all(json,len,...)`** 纯解析版(离线样本重放/单测,不打网络)。 - `app/logic/appstate.c`:新增 `appstate_load_playlist_songs_page(idx, offset, limit)`(`offset==0` 重头清空、`>0` 追加到 `g_st.songs[offset..]`,封顶 `NCM_SONG_MAX`);`appstate_load_playlist_songs(idx)` 保留为兼容整批(内部按页循环)。 - `app/ui/playlist.c`:首屏 `playlist_refresh()` 拉 offset=0 一页(`NCM_PLAYLIST_PAGE=50`);`playlist_tick` 按 `g_ploffset==0` 决定「回顶/保滚动」;新增 `pl_maybe_load_more()`(触底且 `g_pl_more` 且未到 `NCM_SONG_MAX` 时加载下一页), 在 `playlist_touch_move`/`playlist_swipe` 滚动后触发;追加失败仅 `ncm_snack("加载失败")` 并保留已载歌曲(不整页清空)。 - `app/ncm_app_internal.h`:`NCM_PLAYLIST_PAGE 50`;声明分页函数 + `lyric_state`。 ### 12.4 UI 遗留修复 - **歌词字面 `\n`**(`app/logic/lyric.c`):`lrc_parse` 复制正文改经 `copy_lrc_text()`——把字面 `\n`/`\r` (服务器把多行文本以转义折进一行)转空格,`\r\n`/`\n\r` 折叠为单空格;有界 ≤`LRC_MAX_TEXT`。 - **播放页底部提示豆腐块**(`app/ui/player.c`):页脚 `"左右滑切歌 · 点顶部切横屏"` 的 **`·`(U+00B7) 不在 common 字库** → 豆腐块。root cause:common tier 按 GB2312 解码 `A1A4` 得 **`・`(U+30FB)**(gb2312 无 U+00B7,gbk 才有),故 common 图集只有 U+30FB。 已替换为 **`・`(U+30FB)**(在 common 图集内,`meow_font_class_of`→OTHER→路由主槽 slot0 即命中)。其余页面的 `·`(U+00B7) 为同一 bug(settings/login/playlist/search 也有),为避免范围扩散 + 保设计原貌,本期只改播放页页脚(用户实测报点); 已在「诚实出入」标注。 ### 12.5 验证记录(宿主为主;风控纪律:**新增真实 NCM 请求 = 0 次**) | 项 | 方式 | 结果 | |---|---|---| | track_all 离线解析 | `bench/offline_track_all.c`(合成样本,纯解析,0 网络) | **全 PASS**:`songs` 命中、id>2^31 不溢出(`2746301825`)、`al.picUrl` 正确(`song0`/`song1`)、`total=5191`;变体 `playlist.tracks`+`trackCount` 容错命中 | | 歌词字面 `\n` | 宿主编译 `app/logic/lyric.c` + 定向输入(`\\n`/`\\r\\n`) | **PASS**:`"hello\nworld"`→`"hello world"`、`"second\r\nline\nhere"`→`"second line here"` | | App 三页 | 宿主离屏 `MEOW_UI_DUMP`(mock,0 网络) | player/playlist/home 渲染正常(黑胶/进度/控制/歌词、分页 footer `6/128`、红行),无崩溃 | | 歌词后台状态位 | `lyric_state()` 语义 + mock 下 `g_host_mock` 短路(worker 未启、count>0 → 显示歌词) | 正常 | | mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**(仅脚本 echo);`text 812856 / data 840 / bss 1059556` | | qemu-mipsel 冒烟 | 未跑(避免触发真实 unikey 请求;启动路径未改,§11.5 已验) | 未做 | > 注:`out/test_ncm_core_mipsel` 已由 `build_core.sh` 重建(含新增 `test_track_all_parse`),但**未运行**(其 `test_network` > 会打真实 NCM 请求);track_all 解析已由 `bench/offline_track_all`(0 网络)覆盖。 ### 12.6 md5(自有源码 + 二进制) ``` 9c4a79f669967d193cba9fdaa41f8442 ncm # mipsel 静态 App(新) 9d1ff1ad5027c1932c7f4f0163df8b92 core/ncm_api.h 6b2daef7a48215fcd7369351e09ee25f core/ncm_api.c 8067ef74af7db49340b693ec62c654f0 app/ncm_app_internal.h 7e54b48012fdfb392ba5910d22153078 app/logic/appstate.c c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e0413031bb8613ae app/play/player_manager.c 9bf68c8420bd8c819cf318b5a16d3d9f app/ui/player.c 612f6e92cade6a59d33f3b5d6aee7b13 app/logic/lyric.c 58ee655751a8efb69df850dc96afdcc1 test/test_ncm_core.c 61b019c64a805e8873db21482def3408 bench/offline_track_all.c ``` ### 12.7 出入清单(只改 `patch_v2/ncm_app/`) ``` 修改: core/ncm_api.h .c # ncm_song_t.al_pic + parse_songs_array(解析 al.picUrl) # + ncm_api_playlist_track_all(分页) + ncm_api_parse_track_all(离线解析) app/ncm_app_internal.h # NCM_PLAYLIST_PAGE + appstate_load_playlist_songs_page + lyric_state app/logic/appstate.c # 分页加载 appstate_load_playlist_songs_page(兼容整批保留) app/ui/playlist.c # 触底加载更多(50/页,cap 256);追加失败保留已载 app/play/player_manager.c # 歌词持久 worker + 异步 load_lyric + lyric_state app/ui/player.c # 黑胶封面用 al_pic;异步歌词空态(loading/no lyric);页脚 ·→・ app/logic/lyric.c # LRC 字面 \n 转空格(copy_lrc_text) test/test_ncm_core.c # 新增 test_track_all_parse 离线断言 新增: bench/offline_track_all.c # track_all 离线样本重放(0 网络) REPORT.md # 本批记录(§12) (未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机/动设备) ``` ### 12.8 诚实出入 / 已知限制 - **未真机验证**:本机无可达设备主机(与 §11.5 同:`meowos.local` 不解析、无设备别名/IP),故未真机跑。 二进制已就绪(md5 `9c4a79...`),真机部署 + 验收(1-2 次请求)留用户执行: `scp ncm root@:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new && ssh root@ 'mv ...ncm.new ...ncm'`(先 .new 再 mv)。 - **`/api/playlist/track/all` 端点形态未 live 验证**(无宿主 cookie,且 NCM 真实 + 风控纪律不允许多请求): 请求/解析按 ncm-api-rs 生成的 eapi 路由 `/playlist/track/all` + API.md 的 `limit/offset` 文档语义实现, 并对数组路径(`songs→playlist.tracks→data.songs`)与 `total` 做了**多路径容错**;若真实响应字段有出入, 解析器会据实际样本在下一轮对齐(离线单测已覆盖本实现所依据的形态)。 - **http 封面 URL 旧疾**:`core/ncm_http.c` 对 `http://` 也走 TLS 握手(当前实现只判 scheme 再统一 TLS), 若个别 `al.picUrl`/`coverImgUrl` 为纯 http 会握手失败。这是 **v1 同源遗留**(v1 已用 coverImgUrl), 本期未改 core(避免动已验证的 TLS 层);实测 NCM 专辑/歌单封面多为 https,可后续统一跳转 https 或加 http 明文分支。 - **页脚等 `·`(U+00B7) 残留**:settings/login/playlist/search 的 `·` 同为 common 缺字 → 会豆腐块(用户仅报播放页, 本期修播放页);如需全面修,把全 App 的 U+00B7 换成 U+30FB 即可(同一 bug,非功能缺失)。 - **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走 `meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意), 本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。 - **`song_ids[]` 字段曾在 §12 被闲置**(`track_all` 直返歌曲对象);§13 已回退 detail+song_detail,重新用于 暂存 `playlist/detail` 返回的 trackIds 以驱动 `song_detail` 分批(§13 为此更新)。 - **歌词竞态**:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。 - **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**, 仅宿主 glibc 触发;mipsel musl 无此告警,`build_ncm.sh` 0 warning)。 --- ## 13. 真机两修:歌单详情 Load Failed 根因(track/all 端点不存在)+ 歌单列表误触(2026-08-31,已真机闭环) > 现象(用户真机验收):① 登录/主页 64 歌单正常,但**进任意歌单都 Load Failed**——§12.3 新换的 > `/api/playlist/track/all` 分页端点从未真机验过,真机响应字段存疑(§12.8 已自标风险); > ② **偶发误触**:歌单列表点到非触摸坐标的行(列表触摸坐标有偏差/串行)。 > 本批先真机用最小请求量抓 track/all 真实响应样本、对齐结论,再修复并用真机验证。仅改 `patch_v2/ncm_app/`, > 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区。 ### 13.1 根因①:`/api/playlist/track/all` 端点根本不存在(404「接口未找到」) 按 §12.8 的“真机抓真实响应样本对齐”计划:设备侧链路(`bench/probe_track_all.c`,用与 App 相同的 `ncm_req_build`+`ncm_http_request`)对小歌单 `18206253175`(Docu/1 首)抓**原始响应体**。为把“端点是否存在/响应字段” 与“解析对错”解耦,探针**绕过解析器**直发请求,先打原始 body。三次(风控 ≤3)真实请求结果: | 路由 | 加密 | URL | 原始响应 | |---|---|---|---| | `/api/playlist/track/all` | eapi | `https://interface.music.163.com/eapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | | `/api/v6/playlist/track/all` | weapi | `https://music.163.com/weapi/v6/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | | `/api/playlist/track/all` | weapi | `https://music.163.com/weapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | → **NCM 服务端没有 `track/all` 这条路由**(eapi/weapi、v6/无v6 全 404)。三次请求 body 均为 `{"code":404,"message":"接口未找到!"}`(HTTP 200 但业务 code=404),故 `api_post` 判 `NCM_ERR_HTTP(-10)` → `playlist_worker` 置 `g_load_err` → 任意歌单都 Load Failed。 §12.3 把歌单详情从“验证过的 `/v6/playlist/detail` + `song_detail`(§10.11)”换成基于一条不存在的路由做“服务端分页”, 是回归根因;`ncm_api_parse_track_all` 的“多路径容错”救不了**路由不存在**——收到的是 404 业务码,不是歌曲数组。 ### 13.2 根因②:歌单列表误触 = 拖动滚动后 release 被 SDK 判 TAP,命中“释放点+新 scroll”而非按下行 先验证“触摸坐标与显示是否几何不一致”(任务提示的方向)。设备侧 `bench/probe_input.c`(本地 ioctl,零 NCM 请求): ``` ABS_MT_POSITION_X min=0 max=480 ABS_MT_POSITION_Y min=0 max=720 fb0 xres=480 yres=720 bpp=16 ``` → 触摸面板与帧缓冲 **1:1**(无缩放/无固定偏移),故“坐标有偏差”不是面板标定/缩放问题,App 侧命中公式 (`w->y + w->scroll` 与绘同式)也一致——命中数学本身是对的。 真正根因是**手势时序**:SDK 抬手判 `adx/ady < 65px`(`SWIPE_THRESH`)为 TAP;而拖动过程中 `touch_move` 已 `meow_listview_scroll_by(-dy)` **滚动列表**。于是“小拖动(0..64px)= 列表已滚 + release 仍触 TAP”, `playlist_on_tap`/`home_on_tap` 用 `meow_listview_hit(释放x, 释放y)` = **“释放点 + 更新后的 scroll”**命中, 打到的是“已经滚动到手指下方”的那一行,与用户**按下**的行不一致 → “点到非触摸坐标的行”(偶发,手指微动即触发)。 (§12.5 宿主离屏 `MEOW_UI_DUMP` 只验静态渲染、不发真实触摸,故未暴露。) ### 13.3 修法 - **Load Failed**(`app/logic/appstate.c`):`appstate_load_playlist_songs_page` 回退 §10.11 已验证路径—— `/api/v6/playlist/detail`(`n=1`,取前 `NCM_SONG_MAX` 首 trackIds)+ `/api/v3/song/detail` 分批取歌曲对象 (`parse_songs_array` 仍带出 `al.picUrl`,保留 §12.1 专辑封面)。`offset==0` 重头整批载入(idn≤NCM_SONG_MAX); `offset>0`(触底“加页”)只要 `NCM_OK` 无操作——detail 一次已取满至上限,无独立分页。 - **误触**(`app/ui/home.c` 与 `app/ui/playlist.c`):点行改用**按下行** `g_press_row`(touch_down 命中行, 与“红条按压”同源);手指移动量 `g_press_delta > 10`(与按压红条清行同阈值)判为**滚动收尾**,不触发点行。 干净点击仍播放按下行(更符合“按哪行播哪行”);拖动手势只滚动不再误播。`search.c` 同款模式, 非本次报点,未改(避免扩范围,见 §13.8)。 - 仅在 `patch_v2/ncm_app/`;`app/`/`core/` 均未动其它逻辑;`ncm_api_playlist_track_all`/`ncm_api_parse_track_all` 保留在 core(现为不可达死代码;`offline_track_all`/`test_track_all_parse` 仍可作离线回归)。 ### 13.4 真机验证记录 **触摸=fb**(probe_input,本地 ioctl):`ABS_MT[0,480]×[0,720]`,`fb0 480×720 16bpp` → 1:1,排除标定。 **修复后数据链路**(`bench/probe_fix.c`,复刻修复后 `appstate_load_playlist_songs_page(offset=0)` 的 detail→song_detail 完整路径;device_id=`BD6DE5…`, cookie_len=1842): ``` === small (Docu/1, 18206253175) === detail rc=0 elapsed_ms=1175 idn=1 ids[0]=2048962657 song off=0 batch=1 rc=0 elapsed_ms=717 bn=1 RESULT song_count=1 [0] Verve (feat. FJØRA) / Ryan Taubert / Verve (feat. FJØRA) FIX VERIFY OK === big (我喜欢的音乐, 581857874, 5191) === detail rc=0 elapsed_ms=34672 idn=256 ids[0]=36190432 song off=0/64/128/192 batch=64 rc=0 bn=64 ×4 RESULT song_count=256 [0] Isla Blanca (Aerosoul Chillout Remix) / Aerosoul / Isla Blanca FIX VERIFY OK ``` → 小歌单 1 首、大歌单 256 首(`NCM_SONG_MAX` 截断,§10.9 上限)**真机都出曲目,不再 Load Failed**。 大歌单 detail 真机 ~34.7s(5191 首 trackIds ≈900KB),但由 `playlist_worker` 后台跑、UI 显示 LOADING 不冻结(§10.11 P1 已修)。 **部署二进制冒烟**(`ncm.new`→`mv`,起 8s 再 SIGTERM): ``` start → home: playlists rc=0 count=120 uid=408996274 → exit ``` → 新二进制启动正常、主页加载 120 歌单、干净退出,无崩溃。 **请求量**:track/all 诊断恰 3 次(风控 ≤3);修复后验证走 detail+song_detail(验收必需)。 真实请求总计数:诊断 3(track/all)+ 复验 7(small detail/song 2 + big detail/4×song 5)+ 冒烟 1(user/playlist)= **≈11 次**, 均为必要开销。另有一次无效运行(data_dir 误传致 TLS 失败),未发服务器请求。 ### 13.5 真机部署 `/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm`(先 `ncm.new` 再 `mv`;`ncm.bak2`=§12 旧回归二进制, `ncm.bak`=§10.11 前)。**新 md5 `734221661ce547393c03ae3568ca3f36`**(旧 §12 `9c4a79f669967d193cba9fdaa41f8442`)。 ### 13.6 出入清单(只改 `patch_v2/ncm_app/`) ``` 修改: app/logic/appstate.c # track_all 404 回归:appstate_load_playlist_songs_page 回退 detail+song_detail app/ui/playlist.c # 误触:点行用按下行 g_press_row + 拖动(g_press_delta>10)抑制 app/ui/home.c # 误触(同 playlist.c):点行用按下行 + 拖动抑制 新增: bench/probe_track_all.c # track_all 原始响应探针(诊断 404;抓样本用) bench/probe_input.c # 触摸 ABS 范围 + fb0 分辨率探针(本地 ioctl,零 NCM) bench/probe_fix.c # 修复后数据链路真机复验(detail→song_detail) REPORT.md # 本批记录(§13) (未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区) ``` ### 13.7 md5(本批改动文件 + 产物) ``` 734221661ce547393c03ae3568ca3f36 ncm # mipsel 静态 App(新;已部署设备) 453eeb5d29809854061d8e4e0300855a app/logic/appstate.c 2ce92315d0e27492b36374f512fabaeb app/ui/playlist.c 4217763fbf766c77917f86f63abb634c app/ui/home.c 279ebbcfca65f9225b93b04213f8fde4 bench/probe_track_all.c 7dc88e448fb10a6da24ad48511023307 bench/probe_input.c 5ad3107059bca1f8e14776afdf6abf82 bench/probe_fix.c ``` ### 13.8 诚实出入 / 已知限制 - **`track/all` 不存在使 §12.3 的整体翻页目标落空**:`/api/playlist/track/all`(eapi/weapi、v6/无v6)真机全 404, 故“服务端分页 + 第 256 首之后继续加载”无法实现。本期回退为 §10.11 的 detail(全量 trackIds,浏览器首屏 ~34.7s, UI 后台 LOADING 不冻结)+ `NCM_SONG_MAX=256` 截断。若仍要超过 256 首滚动加载,只能改用 detail 已返回的全量 trackIds 驱动 song_detail 分页(不依赖 `track/all`),列入后续;本期只恢复到“能出曲目”的可靠基线。 - **`ncm_api_playlist_track_all`/`ncm_api_parse_track_all` 现为不可达死代码**(服务端无路由),保留以不破坏 `offline_track_all.c`/`test_track_all_parse`(离线样本回归,0 网络)。后续可连同 §12.3 一并清理。 - **误触修复未真机触摸复现**:无法经 SSH 驱动真实触摸;修复基于「按下行 + 拖动抑制」的明确逻辑 + 触摸=fb 1:1 的 实测佐证(排除标定)。建议用户真机复验:快速横滑列表后抬手,应只滚动不误播;干净点行仍播按下行。 - **`search.c` 列表点行同款误触模式**(未报点,本期未改,避免扩范围):`search_on_tap` 仍用 `meow_listview_hit(释放点)`; 如需一并修,套用 §13.3 同名改法即可。 - **大歌单首屏 ~34.7s**:detail 全量 trackIds(≈900KB)在 MIPS32 上解析慢(§10.11 已记录,宿主 4.7s,~7×);后台 worker 避免冻 UI。已恢复到 §10.11 用户接受的基线。 - **`al.picUrl` 封面保留**:song_detail 经 `parse_songs_array` 解析出 `al.picUrl`,§12.1 黑胶真专辑封面功能不退。 - **构建**:`build_ncm.sh` mipsel 自有代码 `-Wall -Wextra` 0 warning(本批改动文件逐文件复核无告警);vendor 豁免。 - **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区**;部署留用户后续验收。 --- ## 14. 三期第一批:歌单真分页 + 专辑封面 ncm_cache 化 + 歌词后台加载补锁(2026-08-31,agent-239) > 范围:仅 `patch_v2/ncm_app/`(含 build 脚本与 REPORT 本文件)。未动 `sdk/`(主会话在改 `meow_ui_ime.c`)、 > `meow_launcherd/`、`vendor/`、`core/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/宿主导出/已落盘样本)。 > 部署/验收留用户(真机验收点见 §14.7)。 ### 14.1 歌单分页(detail 全量 trackIds + song_detail 分批,取消 256 硬截断,分页驱动) - **背景**:§13.1 真机确认 `/api/playlist/track/all`(eapi/weapi、v6/无v6)全 404,§12.3 的“服务端分页”不可行; §13 回退到 detail(`n=1`)+ song_detail 但 `NCM_SONG_MAX=256` 硬截断、一次拉满(首屏 34.7s)。本期把 “256 截断 + 一次拉满”改为“**detail 全量 trackIds 驱动 + song_detail 分批 + 触底逐页加载**”。 - `app/logic/appstate.c` `appstate_load_playlist_songs_page(idx,offset,limit)` 重写: - `offset==0`(重头):detail(`n=1`) 取全量 trackIds 入 `song_ids[]`(≤ `NCM_SONG_MAX`),记 `track_ids_count`; `song_count`/`songs[]` 清空;只载**第 0 页**(song_detail 前 `limit` 首,其余待触底再拉,不冻 UI、不一次拉满)。 - `offset>0`(触底追加):从 `song_ids[offset..]` 载 song_detail,追加写 `songs[offset..]`, `song_count = offset + 本页实际数`;**失败返回不修改 song_count**(保留已载页)。`offset>=track_ids_count` 或 `offset>=NCM_SONG_MAX` 时无操作返回。 - `track_ids_count` 为“是否还有下一页”唯一判据(`song_count < track_ids_count`)。 - `app/ui/playlist.c`:`playlist_worker` 按页(`NCM_SONG_BATCH`)调 `appstate_call`;`g_pl_more = (rc==NCM_OK) && song_countal_pic[0] ? s->al_pic : g_st.cover_url`(§12.1),**歌单列表/详情头块封面保持现状**(`cover_get()`,未改)。 - `app/logic/cover.c` 封面图改走 **ncm_cache**(LRU 口径沿用 ncm_cache):键 = djb2(封面 URL) → 文件 `song_cov.jpg`(与 mp3 `song_.mp3` 同池,`ncm_cache_lookup`/`commit` 刷新 mtime=LRU)。 worker 流程:① 缓存命中直接解码(免下载、二次进入离线可看);② 未命中 → 下载解码 → `ncm_cache_open/write/commit` 落盘;仍写 `cover_current.jpg`(`save_cover_raw`)供 launcher 媒体卡片。 - **顺带修真 bug**:旧 `save_cover_raw` 在已持 `g_cm` 的临界区内再 `pthread_mutex_lock(&g_cm)`(非递归锁自锁死)→ cover worker 永久卡死、`g_gen` 永不 +1 → 黑胶封面**永不刷新**。改为“调用方持有 `g_cm`”(不再二次取锁); 封面写盘缓冲扩到 `NCM_CACHE_DIR_MAX+32`(顺带消宿主 glibc fortify `-Wformat-truncation` 告警,mipsel musl 本就无)。 ### 14.3 歌词后台加载(确认已异步;补数据竞争锁) - 歌词后台加载 §11(`player_manager.c` `lyric_worker`:轮询当前曲 → 异步 `ncm_api_lyric` → `lyric_parse`, UI 不阻塞;`lyric_state()` 判「加载中/无词」)**已落地**,本批未改后台线程模型(确实不阻塞 UI)。 - 补 `app/logic/lyric.c` **数据竞争锁** `g_lyric_mu`:写侧 `lyric_parse` 整段持锁(清空用 `lyric_clear_nolock` 避免二次取锁自锁死);读侧 `lyric_count`/`lyric_line_at`/`lyric_line_text`/`lyric_line_tl` 每次取锁。 消除 UI 线程(player.c 歌词区)与 IPC 线程(ipc_media.c `lyric_count`)同 lyric worker 间的数据竞争 (早前为“良性竞态”§10.9 FM4;本期按“禁止数据竞争”要求收紧)。 - 与现有 `g_play.mu` 同模式(函数级锁、无嵌套)。已知残余:worker 读 `g_st.songs[idx].id` 无锁—— `songs[]` 写读竞态(§10.9 FM4 接受,与播放线程同源),本期未扩范围。 ### 14.4 验证记录(0 真实 NCM 请求) | 项 | 方式 | 结果 | |---|---|---| | 歌单分页状态机 | `bench/pagination_sim.c` 离线 stub(0 网络)驱动**真实** `appstate_load_playlist_songs_page` | **全 PASS**:130 首读全程(1 detail + 2 song_detail 页,`song_count=130/track_ids_count=130`,末首含 al_pic);5191 首封顶 `NCM_SONG_MAX`(1000,10 页,越界无操作);空歌单 0;越过尾部/上限再 load 均无副作用 | | mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 842820 / data 856 / bss 2052068`(bss +~1MB 来自 NCM_SONG_MAX 1000) | | 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | home/playlist/player/lands 渲染正常(歌单 6 行 + footer `6/128`、黑胶 + 歌词当前行红色高亮),无崩溃 | | 宿主链接 | `build_ncm_host.sh` | 补 `meow_pinyin.o`(主会话 229 拼音 IME 引入 `py_predict`,旧 host SDK_SRC 漏编 → 链接断);cover.c 截断告警已消 | ### 14.5 md5(本批改动文件 + 产物) ``` b34bc1ed2c3539f1ca80a005884833cd ncm # mipsel 静态 App(新) 1a2327e0ccd352d11bea88ebd35df549 ncm_host # 宿主离屏调试二进制 0b0a4dd5e208d7b5eb504befb374a29e app/logic/appstate.c 6d66eed99c556e301498c5823b43c294 app/logic/cover.c ace0def0e8c3ec991013b0675957f269 app/logic/lyric.c 1a1871032c2ecae7a1cc80c7aa00381a app/ncm_app_internal.h ab87bd5f74b82fd3d62c49a16a4c9fe5 app/ui/playlist.c c73366373b3a372d96b2218e528c6381 build_ncm_host.sh 141e9098a7cabeade9aa6b442ba20910 bench/pagination_sim.c ``` ### 14.6 出入清单(只改 `patch_v2/ncm_app/`) ``` 修改: app/logic/appstate.c # 歌单分页:offset=0 重建 trackIds 索引+载第0页;offset>0 追加;track_ids_count app/ui/playlist.c # 触底逐页加载 + g_pl_more 判据 + 加载/失败/空态 + 点按重试 + 追加失败保留已载 app/ncm_app_internal.h # NCM_SONG_MAX 1000 / NCM_SONG_BATCH 100 / 删 NCM_PLAYLIST_PAGE + track_ids_count app/logic/cover.c # 封面走 ncm_cache(LRU) + 修 save_cover_raw 自锁死 + 缓冲扩容消截断告警 app/logic/lyric.c # 加 g_lyric_mu(写 parse 整段持锁、读逐次持锁)消除数据竞争 build_ncm_host.sh # host SDK_SRC 补 meow_pinyin(拼音 IME py_predict 漏编→链接断) ncm / ncm_host # 重编产物 新增: bench/pagination_sim.c # 歌单分页离线状态机回归(stub 网络端点,0 请求) REPORT.md # 本批记录(§14) (未动 sdk/、meow_launcherd/、vendor/、core/;未 commit/push/刷机;未动设备系统/恢复区) ``` ### 14.7 诚实出入 / 已知限制 - **真机未部署/未验**(风控:0 新增真实请求)。部署链路同 §13.5(`scp ncm root@:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new` → `ssh root@ '. /etc/profile; mv ...ncm.new ...ncm'`,先 `.new` 再 `mv` 防 ETXTBSY)。真机验收点: ① 小歌单全程可读;大歌单(如 5191)下滑触底逐页加载到 1000 封顶(footer `已载/总数`),不冻 UI; ② 黑胶显示真实专辑封面(首次下载→ncm_cache 落盘,二次进入免下载;第三方封面 `http://` 若握手失败回落歌单封面, 为 §12.8 既有“http 封面走 TLS”旧疾,本批未改 core 规避);③ 歌词后台加载不卡 UI、切歌刷新、下/上一首顺延。 - **大歌单仍有内存上限(NCM_SONG_MAX=1000)**:这是 14MB 硬约束下的取舍(每 +100 首 ≈ +132KB)。 若要全量 5000+ 首,需:a) 歌曲对象瘦身(`al_pic[512]` 每首占大头,可拆到按需加载),或 b) 列表滑动窗口 (`meow_listview` 需 O(1) `src2(idx)` 重映射,复杂度高)。footer「已载/总数」已向用户标明进度。 - **`ncm_cache_evict` 生产路径未显式触发超限淘汰**(§10.9 既有遗留:evict 仅 test 调用;song/cover commit 后不自动 触发)。封面走 ncm_cache 同池后同样受该路径约束——**超限淘汰非本批引入**,本批也未扩大行为(不主动加 evict 调用, 避免改已验证的缓存写入路径)。 - **歌词锁残余**:读侧函数取锁后返回存量指针(`s_txt/s_tl` 静态数组),解锁后可能被下一次 `lyric_parse` 覆盖; 写入仅发生在切歌瞬间(一次/歌),本地读在一两帧内完成,峰值可能显示一帧旧/空行(与 §10.9 FM4 同接受)。 严格消除需调用方持锁拷贝(本批未做,接口需改,避免扩大 diff)。 - **host 构建有 SDK `meow_ui_vol.c`「system 返回值忽略」告警**(SDK 源,非本 App;mipsel musl 无)。本 App 自有代码 0 warning。 - **分页触底加载与误触/点按为逻辑推断**(无法经 SSH 驱动真实触摸/滚动);误触修复(§13.3)未在分页下回归真机。 建议真机快速下滑到底验证逐页加载、点按重试、追加失败保留已载。 ### 14.8 数值权衡(供后续微调) | 常量 | 旧值 | 新值 | 说明 | |---|---|---|---| | `NCM_SONG_MAX` | 256 | 1000 | 单页渲染上限(分页驱动),歌曲数组 ~1.32KB/首 → +100 ≈ +132KB bss | | `NCM_SONG_BATCH` | 64 | 100 | song_detail 每批(≤500 安全窗内保守 100;`clist[4096]` 装 100 首 ≈2KB) | | `NCM_PLAYLIST_PAGE` | 50 | 删除 | track/all 服务端分页遗留(该端点 404),改由 `NCM_SONG_BATCH` 承担页粒度 | | `g_st.track_ids_count` | — | 新增 | detail 取回 trackIds 总数,驱动「还有下一页」判据 | --- ## 15. 三期第二批:中文搜索接入拼音 IME + 设置页做好 + 缓存淘汰接线(2026-08-31,agent-241) > 范围:仅 `patch_v2/ncm_app/`(含 `core/`)。未动 `sdk/`(主会话在改 `meow_ui_theme.c`/测试,另有 agent 在改 > `meow_launcherd/` 主题)与 `meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/宿主 mock/ > 已落盘样本)。部署/验收留用户(真机验收点见 §15.7)。上一批(歌单分页/封面/歌词锁)已 commit `ee104f1`, > 本批在其上做;提交见 git。 ### 15.1 中文搜索接入拼音 IME - 搜索页(`app/ui/search.c`)输入框 `meow_textfield_create(..., MEOW_IME_ASCII)` 改 **`MEOW_IME_PINYIN`**。 SDK `MEOW_IME_PINYIN`(M2.88 agent-229 落地)打开后为 4 行字母键 + 组合条 + 候选栏(≤6 候选/页,翻页 + 「清」+「拼/英」键),候选上屏走 `py_commit` UTF-8 写进字段并触发 `on_change`(TextField 的 `textfield_change_cb` 实时回写 `g_st.search_query` + `invalidate`)。 - **候选上屏/清空/退格语义**均沿用 SDK IME(`py_commit`/`py_clear`/`py_back_combo`+`py_back_field`,组合态 UTF-8 安全退格 = 删末字;组合非空时退格删组合末字母),App 侧零改动。 - **英文输入零回归**:拼音键盘含「拼/英」键(`K_SWITCH`→ASCII)与 ASCII 键盘顶条「中」热区(→拼音), ASCII/NUM 不随本批改动;搜索仍只在用户点「搜索」按钮时发 `cloudsearch`(风控:进页不自动搜,不逐键发)。 - 保留 `search_submit`/`search_worker`(后台线程 cloudsearch,UI 不卡);`ncm_api_cloudsearch` 的 keyword 为 UTF-8 字符串,中文关键词直接进 weapi 加密体(无需改造)。 ### 15.2 设置页做好(用户定案:音质/缓存上限可设数字+清理缓存+关于) `app/ui/settings.c` 重做为 **5 行**:音质 / 缓存上限 / 清理缓存 / 退出登录 / 关于网易云。 - **音质**:sheet 列出全 5 档(Hi-Res/Lossless/Exhigh/Higher/Standard,默认 Hi-Res)。单曲不可用时 **沿用 core 既有降级链**(`ncm_api_song_url_auto`:hires→lossless→exhigh→higher→standard,`ncm_api.c:260`), 本批未改 core 降级逻辑。 - **缓存上限**:sheet 预设(1/2/5/10/20/30/50/100 GB)+「自定义...」→ INPUT 弹窗 + **NUM IME 数字输入** (`input_cache_limit_gb()`,atoi + 钳制 1~100)。默认 10GB,上限 100GB。持久化走既有 `appstate_save_settings`(`meow_config_*` → `/ncm.cache_limit_gb.cfg`,即 `MeowData/dev.player.ncm/data/`,与 core `ncm_cache_load_setting`/`ncm_store_read_cfg_int` 同目录同键)。 - **清理缓存(新)**:显示当前占用(`ncm_cache_total_bytes`,进页时算一次存 `g_cache_used`,清理后清零); tap → CONFIRM 弹窗 → `ncm_cache_clear`(新 core API)→ snack「缓存已清理」。占用用整数运算格式化 (`fmt_bytes`,GB/MB/KB,不用浮点/软浮点)。 - **退出登录 / 关于/版本行**:保留;关于 tap 改为 snack「网易云音乐 v0.1 · MeowOS 移植版」。 ### 15.3 缓存淘汰接线(ncm_cache_evict 接进生产路径) - **core 新增 `ncm_cache_clear`**(`core/ncm_cache.c/.h`):删除 `song_*` 全部(mp3 与封面同池,含残留 `.tmp`),供「清理缓存」与测试用;`ncm_cache_evict`(既有)保持 LRU=按 mtime 升序删最旧直到 `<= limit`。 - **mp3 下载路径**(`app/play/player_manager.c song_local_file`):**写前**(`ncm_cache_open` 前)与**写后** (`ncm_cache_commit` 后)各调一次 `ncm_cache_evict(&g_st.ctx.cache)`——写前腾空间(此时本线程 temp 尚未建, 无在途文件被误删风险),写后清「新文件推过上限」的增量。刚写的文件 mtime 最新,LRU 淘汰时存活。 - **封面路径**(`app/logic/cover.c` cover_worker 未命中写盘分支):写前 + 写后同样各调一次 `ncm_cache_evict` (封面 `song_cov..jpg` 与 mp3 同池)。 - **手动调低上限**:`appstate_save_settings` 末尾加一次 `ncm_cache_evict`,用户从 50GB 降到 1GB 立即淘汰。 - 上限读设置值:`ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb)`(`player_manager_set_quality` 与 `appstate_save_settings` 均已同步,继 `appstate_load_settings`/`ncm_cache_load_setting`)。 ### 15.4 验证记录(0 真实 NCM 请求) | 项 | 方式 | 结果 | |---|---|---| | 缓存 LRU 淘汰/清空 | `bench/cache_evict_sim.c` 离线(0 网络,稀疏文件构造,直接驱动 core) | **全 PASS**:①超上限删最旧(700+300+400MB→删 700 停于 700MB)②删 2 个最旧到 ≤1GB(4×500MB→留 2)③未超上限无操作 ④`lookup` 刷新 mtime → 被访问的 A 存活、B/C 被淘汰 ⑤`set_limit_gb` 钳制(0→1、200→100)⑥`clear` 删 mp3+封面+残留 .tmp(total=0)| | 歌单分页回归 | `pagination_sim`(0 网络) | **全 PASS**(130/5191/空 三态无回归;`appstate.c` 加 `ncm_cache_evict` 未破坏分页——`--gc-sections` 丢弃不可达的 `save_settings`,链接不受影响) | | core 宿主测试 | `test_ncm_core_host`(重建 core-host) | **PASS** | | core mipsel 测试 | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS** | | mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 844640 / data 856 / bss 2052068` | | 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | settings(明/暗 5 行 + 占用 + 版本 + 品牌红脚注)/search(输入框+搜索钮+footer)/home/playlist/player 均正常渲染,无崩溃 | ### 15.5 md5(本批改动文件 + 产物) ``` 8a313d27dc7d05fa3a78ce7772436b04 ncm # mipsel 静态 App(新) dc66bdcfb29ba5717b42c72ecbf45049 ncm_host # 宿主离屏调试二进制(重编) be3d906edce4d90ebd9c09d3b7faf814 core/ncm_cache.c # 新增 ncm_cache_clear f5810d23709080f4fa39b2fbc242c9e0 core/ncm_cache.h 41ae3f40014f68f7855fed3a0be1558c app/ui/settings.c # 设置页 5 行 + 自定义数字 + 清理缓存 9811af8f7150b325225e067748ca50a3 app/ui/search.c # 输入框 MEOW_IME_PINYIN 2350622237e9fb5d05a5497dd8b87e39 app/play/player_manager.c # mp3 写前/写后淘汰 fb1a74a68950b26a2a4f42281725f37d app/logic/cover.c # 封面写前/写后淘汰 a51b6d8477bde5692089caf511eafca1 app/logic/appstate.c # save_settings 末触发淘汰 ed2f71c52563e812a04fb8a7cc05148d app/test/test_ncm_app.c # settings mock 初始化缓存目录 2134ad23a6ee4d1b37120ee8d2e44337 bench/cache_evict_sim.c # 新增 LRU/清空离线回归 ``` ### 15.6 出入清单(只改 `patch_v2/ncm_app/`) ``` 修改: app/ui/settings.c # 设置页 5 行:音质(全5档)/缓存上限(预设+自定义数字)/清理缓存(显示占用)/退出/关于 app/ui/search.c # 搜索输入框 MEOW_IME_ASCII → MEOW_IME_PINYIN(中文搜索,英文零回归) app/play/player_manager.c # song_local_file 写前/写后 ncm_cache_evict app/logic/cover.c # cover_worker 未命中写盘分支 写前/写后 ncm_cache_evict app/logic/appstate.c # appstate_save_settings 末尾触发 ncm_cache_evict(手动调低上限立即淘汰) core/ncm_cache.c/.h # 新增 ncm_cache_clear(删 song_* 全部) app/test/test_ncm_app.c # settings mock 初始化 ctx.cache(「占用」行读真实目录) ncm / ncm_host / test/test_ncm_core_host # 重编产物 新增: bench/cache_evict_sim.c # 缓存 LRU 淘汰/清空 离线回归(0 网络) REPORT.md # 本批记录(§15) (未动 sdk/、meow_launcherd/、vendor/;未动设备系统/恢复区;未 killall hiby_player / 未重启设备) ``` ### 15.7 诚实出入 / 已知限制 - **真机未部署/未验**(风控:0 新增真实请求)。部署链路同 §13.5(`scp ncm root@:/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm.new` → `ssh root@ '. /etc/profile; mv ...ncm.new ...ncm'`)。真机验收点: ① 搜索页输入拼音→候选上屏中文→点「搜索」出中文结果(键盘「拼/英」「中」「退格」「清」语义); ② 设置页缓存上限「自定义」数字键盘输入 1~100、改上限后立即 LRU 淘汰;③ 清理缓存显示真实占用、(超上限 磁盘占用下降);④ 音质默认 Hi-Res、VIP/版权曲逐档降级播放。 - **`song_..mp3` 双点命名是 core 既有约定**(`ncm_cache_path` 用 `"song_%s.%s"` + ext 带前导点 `.mp3`) ——本批未改(改动它需动 core 缓存写入路径,风险大于收益);`cache_evict_sim.c` 已按此约定构造文件, 验证的正是真实生产路径。 - **跨线程淘汰的极小竞态**:cover 线程写盘时,player 线程的下载 temp(`song_..mp3.tmp`)也匹配 `song_` 前缀;但 in-progress .tmp 的 mtime 是「刚写」=最新,LRU 升序下排最末,正常淘汰只删最旧, 不会先删它。仅在「池严重超上限、删净旧文件后仍超」的极端情形才可能触及其(上限 ≥1GB,现实中几乎不 发生);沿用既有「双线程同池无锁」模型(本批未扩范围,未引入新锁)。 - **`ncm_cache_evict` 上限读 `&g_st.ctx.cache`**:宿主 settings mock 未 `ncm_ctx_init`(只 init cache dir), `ctx.cache.dir` 为空 → 占用行显示 0(渲染验证用);真机 `appstate_init`→`ncm_ctx_init` 后为真实目录。 ### 15.8 数值权衡(供后续微调) | 常量 | 旧值 | 新值 | 说明 | |---|---|---|---| | 设置页行数 | 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 的多次重启均为 被测路径触发的设备自重启)。