feat(ncm): 新写入的缓存文件嵌入歌曲元数据(FLAC/MP3,commit 前流式重写,失败降级裸流)

用户需求:保留元数据分离的同时把歌曲元数据嵌进缓存文件(否则外部播放器读不到任何信息)。
- 接入点 player_manager:下载完整(dl_rc==OK)→ stream_src_finish(读端先自然收尾)→
  ncm_tag_embed_file(.tmp, 实际扩展名, {name/art_name/album_name}) → dispose join 之后才
  commit。tag 重写是同目录 .tag.tmp + rename 换 inode,解码端全程持旧 fd,播放/seek/
  .part 续传字节语义零改动。
- 容器按实际 magic 分支(ncm_tag.c);峰值内存 64KB;失败只记 WARN、commit 裸流,
  播放主链路不受影响(用户红线)。
- 元数据分离体系不动:<cache_dir>/meta/<id>.bin 旁路照旧写读;LRU 只 stat 文件,
  无 size/etag 索引需回填(重写后 stat 自然一致)。
- test_ncm_core 新增 test_cache_tag:writer 写裸流 → 嵌入 ID3v2.3 / VORBIS_COMMENT →
  读回 → ncm_cache_scan(hires 名 + 320k mp3 体 → 实际档 EXHIGH,名称来自文件内 tag)
  → ncm_cache_remove → ncm_cache_clear。
- REPORT §38:设计/内存估算/验证/诚实遗留(本机缺 vendor 与交叉工具链,未跑整链与真机)。
This commit is contained in:
AskaEth committed 2026-11-04 20:49:25 +08:00
1 parent f392ee5c48
commit 39465ea1ef
3 files changed
+234 -1

No files matched your search

+76
View File
@@ -3293,3 +3293,79 @@ d102c64f7fab73e981a789c5960e6bd9 ncm_host # 宿主调试二进
- **`account.json` 旧账号首次升级**:旧文件无 `last_refresh_ts` 字段 → `cookie_last_refresh_ts=0` → 视为临期,开机续期一次;成功即落盘,避免反复判临期。
- **eapi 会员播放(song_url 等)因 MUSIC_U 头注入而改变行为**:此前 eapi 请求不带 MUSIC_U(§36.5 遗留),登录态 eapi 播放按游客鉴权;本批起带真实 MUSIC_U,会员档/登录态更准确——需在 eapi 可达网络下观察歌单/播放回归(`music.163.com` 域不受影响)。
- **未部署**:`ncm` 产物未上卡;真机仅做了只读探针(不动 App 二进制),`app.json`/`ncm` 未替换。
## 38. M5:已缓存列表页 + 缓存文件内元数据嵌入(2026-11-04)
用户原话:「优化一下网易云的部分逻辑。在首页顶栏新加一个已缓存列表入口。另外,缓存文件的配置改一下:
在保留元数据分离的同时,将歌曲元数据嵌入缓存文件中(不然在其他播放器中读不到任何信息)。」
### 38.1 功能①:首页顶栏「已缓存」入口 + 缓存列表页
- 首页 topbar 新增动作 3(`MEOW_ICON_DOWNLOAD` 位图,`app/ui/home.c`),原「回播放页」顺延为动作 4;
点击 `cache_open()` → push `ACT_CACHE`(新 activity id 7)。
- 新页 `app/ui/cache.c`(对齐 queue.c 惯例:圆角卡 + 红 mini-vinyl + 按压红条 + 当前播放行高亮):
- 行:歌名 / `歌手 · 实际档位 · 大小`;空态「暂无缓存」;页脚「N 首 · 合计 X MB」。
- 点行 = 点播(切 `NCM_QUEUE_CACHE` 队列,上/下一首在缓存列表内循环);行右「删除」= 单曲删除;
顶栏「清空」= 确认框 + `ncm_cache_clear`(与设置页「清理缓存」同语义:音频 + 封面条目)。
- **数据全在 worker**:`cache_reload` 起线程 → `appstate_cache_list_load()` →
`ncm_cache_scan()`(读目录 + 文件头 tag + 音频参数探测)→ 快照;UI 线程只读快照(零磁盘 IO)。
- 播放链路:`NCM_QUEUE_CACHE`(`appstate_queue_count/song`、`player_play_cache_song`);
`player_manager.c` 的 `song_local_file` 对缓存队列用**条目真实键**(id + 文件名请求档 + 实际扩展名)
直接命中缓存文件(不按用户当前档键重下),`eff_level` 回填实际档。
### 38.2 功能②:缓存文件内元数据嵌入(保留元数据分离)
- 新模块 `core/ncm_tag.c/.h`(纯 libc,宿主可单测):
- **FLAC → VORBIS_COMMENT**(type 4,TITLE/ARTIST/ALBUM;插在既有元数据块之后、音频帧之前,
旧 VORBIS_COMMENT 被替换、PADDING/PICTURE 等原样保留);
- **MP3 → ID3v2.3**(TIT2/TPE1/TALB,UTF-16LE + BOM,中英文/emoji 均可;源已带 ID3v2 时先剥旧标签)。
- **按实际容器分支**(读 `fLaC` 魔数)——缓存名里的档位段是**请求档**,真机存在
`song_171650_hires.mp3`(无损请求被降级链落到 exhigh mp3),不能按名字判容器。
- 写入 = 同目录 `<path>.tag.tmp` + 64KB 分块搬运 + fsync + rename 原子替换;失败即 unlink 临时件、
原文件不变 → 调用方 commit 裸流(**降级不影响播放主链路**)。
- 接入点 `app/play/player_manager.c`:下载完整(`dl_rc==OK`)→ `stream_src_finish`(读端先自然收尾)→
`ncm_tag_embed_file(.tmp, ext, {name, art_name, album_name})` → 播放线程 `dispose` join 后才 commit。
解码端全程持有原 inode 的 fd(tag 重写是 rename 换 inode),播放/seek/续传字节语义零改动。
- **元数据分离体系不动**:`<cache_dir>/meta/<id>.bin` 旁路照旧写读;LRU 只 stat 文件(无 size 索引,
故无「回填 size/etag」需求——重写后 stat 自然得到新 size)。
### 38.3 实际档位(用户/塔台取证要求)
`ncm_audio_probe_file`(ncm_tag.c)只读文件头:FLAC STREAMINFO(采样率/位深)、MP3 首帧(码率);
`ncm_cache_scan` 用「请求档 ∩ 实际格式能力」得实际档(只降不升):
FLAC 16bit→无损 SQ;24bit/48k→Hi-Res;24bit/96k→高清臻音;24bit/192k→超清母带;
MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires 名 + mp3 体 → 极高 HQ)。
### 38.4 内存(设备 56MB,硬约束)
| 项 | 峰值 |
|---|---|
| tag 嵌入(每次 commit 前) | malloc **64KB** 分块缓冲 + ~2KB 栈(ID3 构建)+ 结构体 ≈ **68KB 瞬时**,完毕即释放 |
| 缓存页扫描(worker) | 快照临时数组 512×232B ≈ **116KB** + 单文件 tag/探测 64KB 顺序复用 ≈ **180KB 瞬时** |
| 常驻 bss | `g_st.cache_items[512]` ≈ **116KB**(+8.5% 于原 1364516B bss;RSS 占比 <0.3%) |
大文件不整读:FLAC 元数据块/PICTURE、MP3 标签区、音频体全部 ≤64KB 分块;tag 读取只读文件头(≤64KB)。
### 38.5 验证
| 项 | 结果 |
|---|---|
| 新 `test/test_ncm_tag.c` + `build_ncm_tag_host.sh`(FLAC/MP3 构建+读回、中文 roundtrip、旧标签替换、容器判定、3MB 分块、失败降级、音频头探测) | host **51 PASS / 0 FAIL** |
| `test_ncm_core.c` 新增 `test_cache_tag`(writer→embed→read→scan→remove→clear,含 hires 名/mp3 体实际档断言) | 已加入 core 测试集(本机缺 vendor mbedtls/jsmn 无法整链,见 38.6) |
| 临时单测(真实 ncm_cache.c + ncm_api.c + ncm_tag.c,仅桩 `ncm_strlcpy`/`mkdir_p`) | 16 PASS / 0 FAIL(scan 两条、actual=EXHIGH/HIRES、name/artist 来自文件内 tag、remove/clear) |
| 全部改动文件 `gcc -Wall -Wextra`(core + app + tests,-Wpedantic 对 ncm_tag) | **0 warning** |
| `build_ncm.sh` / `build_core.sh` / `build_ncm_host.sh` 源列表 | 已加入 `core/ncm_tag.c`、`app/ui/cache.c`(交叉/宿主编译本机缺工具链与 vendor,未执行) |
### 38.6 诚实出入 / 遗留
- **交叉与整链测试未在本机执行**:worktree 里 `vendor/mbedtls|jsmn|minimp3|qrcodegen` 均被 .gitignore 排除且
本机无交叉工具链(`libs_common.sh` 指向 /home/Aska/Project/R3ProII/.tools)。故 `build_core.sh`/
`build_ncm.sh`/`build_ncm_host.sh` 与本仓库 core 测试**未运行**;已用真实源码单独编译(0 warning)并跑
ncm_tag 全量与临时 cache-scan 单测覆盖。**需在构建环境补跑 `build_core.sh` 的 host+mipsel 两套测试**。
- **未上真机**:不碰真机 Music/MeowData(红线)。ta 嵌入后「外部播放器可读」「minimp3/dr_flac 读带 tag 的
缓存文件仍正常」需真机抽验;minimp3_ex 对 ID3v2 有跳过逻辑(VENDOR_NOTES:「minimp3_ex 带 ID3/CRC 处理」),
dr_flac 原生解析 VORBIS_COMMENT 元数据块,理论兼容,但未实测。
- **tag 重写是一次额外整文件读+写**(TF 顺序 IO):在下载线程内、播放收尾前完成;短曲/快速拖动到底时
dispose 的 join 可能多等 ~0.3–1s。磁盘瞬时要 2× 单曲空间(失败自动降级裸流)。
- **缓存队列点播的时长/封面**取自旁路 `meta/<id>.bin`(单次 ≤600B 读);无旁路的旧条目只有歌名/歌手/大小。
- **列表上限 512 条**(最近访问优先);池内更多时页脚仍显示展示条数,超出部分需 LRU 或搜索触达。