ncm: FLAC 解码落地(dr_flac 0.13.4 单头入库) + 缓存键=歌曲ID+音质档位

- 解码分发 decoder.c:magic(fLaC→dr_flac / 其它→minimp3);flac ≤16bit→S16、
  >16bit→S32 满幅送 audiod(AUDIO_FMT_S32),rate/ch/bits 按实际流,零 SRC 零裁剪
- song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变;flac 下载限幅 1GB
- 缓存键 song_<id>_<level>.<ext>:同歌同档命中不重下,换档新条目旧档留 LRU;
  ext 前导点正名(修双点名),lookup 回退历史双点名+旧无档键温和兼容
- audio_out S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原格式
- 验证(0 真实请求):44.1k/16 与 96k/24 自制样本宿主+qemu 帧数/时长精确、
  逐样本零误差;mp3 回归逐帧一致;player_flow_sim P1-P6(新 P6 档位键/flac ext)、
  cache_evict/cover/pagination/window/audiod_chain sim 全 PASS;mipsel 0 warning
- REPORT §19 / VENDOR_NOTES dr_flac 条目;未部署真机(留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 14:14:38 +08:00
1 parent 80bc191593
commit 70aec677ce
23 files changed
+13377 -110

No files matched your search

+155
View File
@@ -1586,3 +1586,158 @@ fee4204e945866f5a07ca7d191fa83e3 bench/player_flow_sim.c
挂机时封面可能缺席但不影响播放。
- 播放页 BUFFER 态进度条仍画 0:00 标尺(缓冲文案在歌词区),未做进度条一体化——观感项。
- meow_audiod_host 工作区有他人改动(其他 agent 在改 audiod),本批未 add/未提交该文件。
---
## 19. FLAC 解码落地(dr_flac)+ 缓存键 = 歌曲ID+音质档位(2026-09-01)
> 范围:仅 `patch_v2/ncm_app/` + 根 `.gitignore`(vendor/dr_flac 例外条目)。未动 `sdk/`、
> `meow_audiod/`、`meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/sim/
> 自制测试样本;song_url 降级链只改接受条件,未新增请求路径)。
> **本批未部署真机**(部署留主会话统一做)。
### 19.1 动机
播放链此前只有 minimp3(mp3-only):`song_url_auto` 只收 mp3 URL,无损 SQ/Hi-Res 档位
拿到 flac 也只能继续降级 → 无损/Hi-Res 空转(NCM_CACHE_DESIGN §2.5「同日发现」)。
硬件侧 CS43198 支持到 768kHz/S24,audiod 已支持 S16/S24/S32 输入 + DAC 原生率跟随
(AUDIOD_DESIGN §4),缺的只是解码器。本批引入 dr_flac 并打通全链。
### 19.2 dr_flac 引入(vendor/dr_flac/dr_flac.h,入库)
- 来源:https://github.com/mackron/dr_libs(master 分支 `dr_flac.h`,2026-09-01 取得,
532768 B,版本宏 0.13.4);许可证 **public domain / MIT-0 任选**(文件头/尾双许可声明)。
- 单头文件直接入库:`.gitignore` 由 `patch_v2/ncm_app/vendor/` 改为 `vendor/*` +
`!vendor/dr_flac/` 例外(其余 vendor 大树维持不入库,VENDOR_NOTES.md 记版本/获取方式)。
- 集成:`app/play/dr_flac_impl.c` 唯一实现 TU(`DR_FLAC_IMPLEMENTATION` +
`DR_FLAC_NO_OGG`——song_url 下发的 flac 恒原生封装,裁 ogg 容器省 text),
编译豁免告警(与 minimp3 同口径)。
### 19.3 解码分发(app/play/decoder.{h,c},新)
- `ncm_dec_open` 按 **magic** 分发:首 4 字节 `fLaC` → dr_flac;其余(ID3 / 0xFF 帧同步/
未知)→ minimp3(未知格式由 minimp3 open 报错兜底,不加第三分支)。
- 统一接口:`open(回填 rate/ch/bits/total_frames)` / `read(帧)` / `seek_ms` / `close` /
`format`。位宽映射:flac ≤16bit → S16;>16bit → **S32 左对齐满幅**(drflac s32 输出),
送 audiod `AUDIO_FMT_S32`——格式归一/SRC-free 原生率跟随全在 audiod 侧(AUDIOD §4),
本层零裁剪零 SRC。mp3 恒 S16。
- mp3 细节:minimp3_ex 的 `dec.info` 首读后才有效 → open 时预读一块进 stash(4096 帧),
read 先吐 stash;seek 保持旧语义(关+重开按字节近似)。flac seek = 精确 PCM 帧。
- `player_manager.c` run_song 改走 decoder:`audio_out_open(rate, ch, bits)` 按实际流
(不再硬编码 44100/2/16);进度 pos/暂停日志/seek 帧数全部用实际 rate;
song 元数据 duration 缺失时用 `total_frames/rate` 推准确时长(flac 精确)。
单块 PCM 缓冲改静态 32KB(4096 帧 ×2ch ×4B S32 上限;播放线程单实例,省栈)。
### 19.4 audio_out 位宽参数化(S32 通路)
- `audio_out_write` 签名 `const short *` → `const void *`(字节数按 open 的 bits×ch 算,
FILE/ALSA 两后端本就如此)。
- AUDIOD 后端:bits 16→`AUDIO_FMT_S16`、32→`AUDIO_FMT_S32`(其余拒绝);流死重开
(pause resume 失败路径)按本次 bits 还原格式(旧版恒 S16,flac 曲重开会错格式——随本批修)。
- ALSA 直开保底:支持 S32_LE(16/32);WAV sink 头 bits 字段如实写(16/32 均合法 PCM WAV)。
### 19.5 song_url 降级链接收 flac(core/ncm_api.c)
- `ncm_api_song_url_auto` 接受条件:`type ∈ {mp3, flac}`;`type` 缺失(旧端点兼容)时看
url 扩展名(`.flac` → 回填 type=flac),默认当 mp3 收。降级语义不变(hires→lossless→
exhigh→higher→standard 逐档,url null/未知格式继续降)。
- 下载限幅:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB)→ flac 上限 1GB、
mp3 维持 64MB(`u.size` 可信时按 size 限幅)。
### 19.6 缓存键 = 歌曲ID + 音质档位(用户定案 NCM_CACHE_DESIGN §2.5)
- 新键:`song_<id>_<level>.<ext>`(level = `ncm_api_level_str`:standard/higher/exhigh/
lossless/hires)。**同歌同档命中不重复下载;换档=新条目;旧档留给 LRU 自然淘汰**(不主动删)。
键取**用户设定档**(非实际降级产物档)——保证「同设定永不重下」,降级产物归设定档键。
- 查找顺序:新键 flac → 新键 mp3 → 旧无档位键 `song_<id>.mp3`(存量温和兼容,命中即播)。
- **命名正名**:`ncm_cache_path` 剥离 ext 至多一个前导点——旧版调用方传 ".mp3" 生成
双点名 `song_<id>..mp3`(§256 修的既有约定),新文件一律单点;`ncm_cache_lookup`
单点 miss 时回退历史双点名(命中照常刷 LRU,不改名不删除)。封面池(song_cov*.jpg)
同享正名+双点回退。
- autoplay hook(`/tmp/ncm_autoplay`)的「已缓存」判定同步三键顺序。
### 19.7 验证记录(0 真实 NCM 请求)
测试样本:libsndfile(venv soundfile)自制 `out/test-vectors/sweep_44k1_16.flac`
(44.1kHz/16bit/2ch/3.000s=132300 帧)与 `sweep_96k_24.flac`(96kHz/24bit/2ch/2.000s=
192000 帧)(正弦扫频;out/ 不入库,脚本见 §19.9 可复现)。
| 项 | 方式 | 结果 |
|---|---|---|
| flac 44.1k/16 宿主 | `test_decode`(新:走 decoder 分发→audio_out FILE WAV),expect 132300/44100/16 | **PASS**:帧数/率/位宽精确;WAV 数据与 libsndfile 参考**逐样本零误差** |
| flac 96k/24 宿主 | 同上,expect 192000/96000/32 | **PASS**:S32 满幅输出,换算 float 与参考 max err = 0.0;173× RT |
| mp3 回归 宿主 | MIPSTest.mp3 expect 393984/48000/16 | **PASS**:与 §16 记录逐帧一致(393984 帧/8.208s),WAV 字节数精确 |
| flac+mp3 qemu-mipsel | `test_decode_mipsel`(静态交叉)同三样本 | **全 PASS**:44.1k 50.9× RT / 96k24 29.0× RT / mp3 4.8× RT(qemu 模拟保守值,真机更快);MIPS32 LE 端序/32位 long 无问题 |
| 播放状态机 | `bench/player_flow_sim.c`(stub 换 ncm_dec_*;**新增 P6**:档位键+flac ext) | **P1-P6 全 PASS**:P6 实证 cache key=`1000_lossless`、type=flac → `.flac` 落盘;P1-P5 旧用例零回归 |
| 缓存键/命名 | `test_ncm_core`(新增:ext 带点正名单点路径 + 历史双点名 lookup 命中)+ `cache_evict_sim` | 宿主 + qemu-mipsel **全 PASS**;evict 6 组 PASS(`song_*` 前缀扫描不受键变化影响) |
| 封面/滑窗回归 | cover_chain_sim / pagination_sim / window_sim(重编新 ncm_cache.c/appstate.c) | **全 PASS**(封面池同享正名+双点回退;autoplay 三键判定 gc 段不影响 sim) |
| audiod 链回归 | audiod_chain_sim(真实 audiod_host 假 socket,重编新 audio_out.c) | **14/14 PASS**(OPEN/WRITE/PAUSE/RESUME/抢占/流死重开/CLOSE) |
| mipsel 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra;dr_flac_impl vendor 豁免) | **0 warning**;`text 954304 / data 1576 / bss 1113668`(text +85.5KB = dr_flac+decoder) |
| 宿主离屏渲染 | build_ncm_host.sh + 9 页 MEOW_UI_DUMP | 全 rc=0;自有代码 0 warning(SDK meow_ui_vol.c 既有告警除外) |
### 19.8 md5(本批产物 + 改动文件)
```
52d20f5325ba2bb646355dcc6db54ad3 ncm # mipsel 静态 App(新;未部署,留主会话)
ff7acea1b1769fb2e67a5177dfc37d8f ncm_host # 宿主离屏调试二进制
90c7901bff0ff6e2e579ef068ac48161 vendor/dr_flac/dr_flac.h
86e99aaad27bfc828a59694022e4a8aa app/play/decoder.c
b02e8e5f7c7194a6bf492171c137233f app/play/decoder.h
2c6d5b93cecec980a67b737329847905 app/play/dr_flac_impl.c
6b8b0d13933a547c3349e423a9516eb3 app/play/player_manager.c
6f5ea39ab6b7ef54c87c48916c8705eb app/play/audio_out.c
2ad8b12f47014d034df75f864cb2a9b9 app/ncm_app_internal.h
2ca5b164877617063a7a44964acdf3d8 core/ncm_api.c
de21f105514235594f90427beec0b10f core/ncm_api.h
b870b25c9bd9692c912170ddce2b04de core/ncm_cache.c
0cb40a5af7f1c85af79f4f4b44a9cc4a core/ncm_cache.h
811c730b5fc52256f84b8917b7657649 app/logic/appstate.c
fba1d01fe6f3cfc9bcf334481e8edb09 app/test/test_decode.c
0ed4ce768d82a9512c5d32bca1271366 test/test_ncm_core.c
c6846cb4c31161c42d81560ade628771 bench/player_flow_sim.c
791a9ddbde363348e3ef373931647aeb build_ncm.sh
f4fb97416868b9a61b6b54f8958bc6f9 build_ncm_host.sh
65e3da445996e62079bfa38a9c9699b7 VENDOR_NOTES.md
```
### 19.9 出入清单(只改 `patch_v2/ncm_app/` + 根 .gitignore)
```
新增:
vendor/dr_flac/dr_flac.h # dr_flac 0.13.4 单头(public domain/MIT-0;入库例外)
app/play/decoder.h/.c # 格式分发解码(magic:fLaC→dr_flac / 其它→minimp3;S16/S32 位宽映射)
app/play/dr_flac_impl.c # dr_flac 唯一实现 TU(DR_FLAC_NO_OGG;vendor 豁免告警)
修改:
app/play/player_manager.c # run_song 走 decoder(实际 rate/ch/bits/时长)+ 缓存键 id+档位 +
# flac 落盘(ext 按 type/url)+ flac 下载限幅 1GB + 旧无档键兼容
app/play/audio_out.c # write 改 const void* + S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原
app/ncm_app_internal.h # audio_out_write 签名/注释
core/ncm_api.c/.h # song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变
core/ncm_cache.c/.h # ext 前导点正名(单点)+ lookup 历史双点名回退
app/logic/appstate.c # autoplay 已缓存判定三键顺序(新档键 flac/mp3 → 旧无档键)
app/test/test_decode.c # 改走 decoder 分发 + expect_frames/rate/bits 校验 + meow_ui_time_ms 桩
test/test_ncm_core.c # 缓存命名正名 + 双点旧名兼容用例
bench/player_flow_sim.c # stub 换 ncm_dec_* + ncm_api_level_str/ncm_streq 桩 + P6(档位键/flac ext)
build_ncm.sh / build_ncm_host.sh # +decoder.c +dr_flac_impl.c + -Ivendor/dr_flac
VENDOR_NOTES.md # dr_flac 条目(来源/版本/许可/集成方式/实测)
.gitignore(仓库根) # vendor/ → vendor/* + !vendor/dr_flac/(单头入库)
ncm / ncm_host # 重编产物
(未动 sdk/、meow_audiod/、meow_launcherd/、其余 vendor/;未部署真机;未动设备系统/恢复区)
```
### 19.10 诚实出入 / 遗留
- **真机未部署未验**(留主会话统一)。真机验收点:①无损/Hi-Res 档位真实出 flac 流并出声
(48k/16 与 96k/192k/24 各试一首,日志 `play: start ... fmt=flac <rate>Hz/2ch/16|32bit` +
audiod grant 跟随原生率)②换音质档后同歌重下新条目、旧档仍在(LRU 未触发时)③旧缓存
(无档键/双点名)直接可播不重下 ④flac seek 精确帧。
- **无损档位要会员**:song_url 的 lossless/hires 档需 CVIP/SVIP——**用户账号能否真拿到
flac URL 待真机按账号实测**(非会员会逐级降到 exhigh mp3,行为同旧版,不算回归)。
- 八档音质体系(NCM_CACHE_DESIGN §3 的 jyeffect/sky/jymaster 等 SVIP 档)未进 App
(设置仍 5 档,枚举只到 NCM_LEVEL_HIRES)——缓存键已按档字符串泛化,后续加档零改动。
- dr_flac 只接原生 flac 封装(NO_OGG);song_url 若下发 ogg-flac 会被 sniff 判给 minimp3
报错「decode open failed」——目前未见该形态(type=flac 恒原生),真机若见再加 ogg。
- 96k/24 → S32 满幅在 qemu 下 29× RT,真机 MIPS 预估实时余量充足;若实测吃紧可开
`DR_FLAC_NO_CRC`(省 CRC 校验,~5-10% CPU)。
- 测试样本(out/test-vectors/)与宿主/ffmpeg 工具链(out/tools/)在 out/ 下不入库;
样本可用 python `soundfile` 一键重造(sweep 参数见 §19.7)。