feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ

- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
  + level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
  + song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
  + vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
  刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
  fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
  生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
  test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
  mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 15:55:42 +08:00
1 parent ab4dfb4f9c
commit bdd9e898f4
23 files changed
+1026 -70

No files matched your search

+135
View File
@@ -1741,3 +1741,138 @@ f4fb97416868b9a61b6b54f8958bc6f9 build_ncm_host.sh
`DR_FLAC_NO_CRC`(省 CRC 校验,~5-10% CPU)。
- 测试样本(out/test-vectors/)与宿主/ffmpeg 工具链(out/tools/)在 out/ 下不入库;
样本可用 python `soundfile` 一键重造(sweep 参数见 §19.7)。
## 20. 音质体系全量落地:八档 UI + VIP 检测/解锁 + 逐级降级下首恢复 + fee 门禁 + 默认极高 HQ(2026-09-01)
> 范围:仅 `patch_v2/ncm_app/`。按用户定案 NCM_CACHE_DESIGN §3(八档 UI 权威)+ §2.1
> (VIP 缓存策略)+ CNMPLAYER_BIZ_NOTES §1/§2/§3(端点/9 档映射/fee 口径)实现。
> 风控纪律:本批真实 NCM 请求 **0 次**(vip 解析/降级链全部离线样本 + stub 矩阵)。
> **本批未部署真机**(部署留主会话统一做)。
### 20.1 档位模型(core/ncm_api.h)
- 枚举扩到全档表,**枚举值顺序 = 降级顺序**(高→低):
`SURROUND(8) > SKY(7) > JYMASTER(6) > JYEFFECT(5) > HIRES(4) > LOSSLESS(3) > EXHIGH(2)
> HIGHER(1) > STANDARD(0)`;`DOLBY=16` 映射保留但**不进 UI 清单、不进降级链**;
`HIGHER`(较高 192k)仅降级链中间档,不进 UI。
- UI 八档 ↔ level 串 ↔ 门槛(BIZ_NOTES §2.1 对齐 CNMPlayer 串;中文名以设计文档 §3 为准):
| UI 档 | level 串 | 规格 | 门槛 |
|---|---|---|---|
| 标准 | standard | 128kbps | 免费 |
| 极高 HQ | exhigh | ≤320kbps | 免费(**默认档**) |
| 无损 SQ | lossless | 48kHz/16bit | CVIP |
| Hi-Res | hires | 192kHz/24bit | CVIP |
| 高清臻音 Spatial | jyeffect | 96kHz/24bit | SVIP |
| 臻音全景声 Audio Vivid | sky | Audio Vivid | SVIP |
| 超清母带 Master | jymaster | 192kHz/24bit | SVIP |
| 沉浸环绕声 Surround | sky + `immerseType=c51` | 5.1 声道 | SVIP |
- ⚠️ sky 歧义处理(BIZ_NOTES §2.1 命名歧义的落地决策):CNMPlayer 无独立 Audio Vivid 档,
`sky` 是唯一串且其附加参数 `immerseType=c51` 恰为 5.1 声道 → **臻音全景声=裸 sky、
沉浸环绕声=sky+c51**(song_url 内按档附加)。两档 API 串相同 → 缓存键改用新
`ncm_api_level_key()`(sky / sky_c51 唯一后缀),杜绝两档互串缓存。
- `ncm_api_level_gate()`:standard/higher/exhigh=NONE,lossless/hires=CVIP,其余=SVIP。
### 20.2 VIP 检测与落盘(core vip/info + app/logic/account.c)
- `ncm_api_vip_info`(weapi):先 `/api/music-vip-membership/client/vip/info`(v2,{userId}),
失败/解析失败再 `/front/vip/info`(v1)兜底——**每个触发条件最多 2 次请求,不轮询**。
- 判定(`ncm_api_parse_vip_info`,离线可测):root=data 或 body 本身;
`associator`(黑胶)vipCode>0 / isSign / expireTime>now → **SVIP**;
`musicPackage`(畅听/音乐包)同三判据 + 顶层 redVipLevel/redplusLevel/musicVipLevel/
vipCode>0 或 vipStatus/isVip=true → **CVIP**;SVIP ⊇ CVIP。到期时间双字段原样记录。
- 落盘 `MeowData/dev.player.ncm/data/account.json`:{uid,nickname,vip,cvip_expire_ms,
svip_expire_ms,checked_at}(原子写;启动 account_load 恢复先用缓存渲染)。
- 刷新时机(account_vip_maybe_refresh,调用方 worker 线程上下文,UI 零网络):
①登录成功后(login worker,force)②checked_at 超 1 天 ③会员到期时间已过
(可能自动续费)——**每条件每次启动只查一次**(单飞标志,失败也置位:防风控,
等下次启动/下个条件)。home_worker 歌单拉取成功后触发 ②③ 判定。
- `account_vip()` 为纯本地视图:到期即按非会员对待;登出清 vip 态 + 删 account.json。
### 20.3 逐级降级 + 下首恢复 + 生效档显示
- `ncm_api_song_url_auto` 扩全档表:从 desired 逐档向下(surround→…→standard),
url 空/未知 type 继续降;**out_level 回填实际命中档**;响应 `data[0].level` 无歧义串
(standard..jymaster)且低于请求档时采信响应档(服务端静默降档如实显示),sky 族
歧义保守取请求档。新增 `ncm_api_song_url_auto_f`(try_fn 可注入)供离线 sim。
- 播放路径(player_manager song_local_file):用户档先按 `account_level_cap()` 钳顶
(NONE→EXHIGH / CVIP→HIRES / SVIP→SURROUND)再降级——**超权限档允许选定不硬拒,
但免去注定空 url 的高档请求**(风控)。缓存键仍=用户设定档(降级产物归用户档键,
「同设定永不重下」,§19 语义不变)。下一首从用户档重新起步(链内无记忆)。
- 生效档 `g_play.eff_level`(mu 保护):下载时由 auto 回填;缓存命中按 ext 推断
(mp3 实体+用户档>极高 → 必为降级产物标 EXHIGH;flac 实体按用户档——档内降级
如 hires→lossless 会高估,已知取舍,真实档位无损音质感官一致)。play/next 重置 -1。
- 播放页动态带时间行中央小字:生效档=用户档显示档名,不同则红字「已降级至 xx」
(g_last_eff 沿触发刷带)。
### 20.4 fee 权限门禁(BIZ_NOTES §3 社区口径)
- `song_local_file` 在**缓存查找之后、song_url 之前**:fee∈{1(VIP 才能听),4(付费专)}
且 account_vip()<CVIP → `NCM_ERR_VIP`(新错误码 -15)直接返回,**零 url 请求**
(省一次请求,防风控);播放页显示「VIP 专享,开通会员后可播放」+ snack「VIP 专享歌曲」。
- fee=0/8 放行(8=免费听低音质,播放允许);已缓存的 VIP 曲本地可播不拦
(NCM_CACHE_DESIGN §2.4:下架/权限变化歌曲优先本地缓存)。
- fee 字段本就在 ncm_song_t 且 songmeta bin 原始镜像序列化,盘缓存 round-trip 无需改格式。
### 20.5 UI(设置页八档 + 默认档)
- 设置页音质 sheet 八档(高→低排列),每项「名称 (规格) · CVIP/SVIP(·未解锁)」;
未解锁档**允许选定**(播放时降级),行 sub 标「当前: xx (规格) · 需 CVIP/SVIP,将降级」。
⚠️ meow_sheet 无单项置灰能力(SDK 不在本批范围),置灰以「·未解锁」文字标注代替。
- **默认档 Hi-Res → 极高 HQ**(EXHIGH):仅新装/越界值生效,已持久化的合法旧值原样保留。
- 页脚改「无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级」。
- 档位显示名/规格收敛 chrome.c(`ncm_level_name/ncm_level_spec`,settings/player 共用)。
### 20.6 验证记录(0 真实 NCM 请求)
| 项 | 方式 | 结果 |
|---|---|---|
| vip 解析 9 例 | test_ncm_core(离线合成 JSON:v2 SVIP/CVIP/NONE/已过期/仅 expireTime 有效/v1 扁平 root/redVipLevel/code=301) | **全 PASS**(宿主 + qemu-mipsel) |
| 档位表 | level_str/key/gate 断言(sky vs sky_c51 唯一性) | **PASS** |
| 降级链全矩阵 | **bench/degrade_sim.c**(新,auto_f 注入假 try_fn):desired 9 × avail 9 + 全不可用 9 = **90 例全 PASS**(命中档=min(desired,avail)、请求数=逐档不差一次);钳制(DOLBY→SURROUND/负数→STANDARD)、未知 type 跳过、type 缺失按扩展名收 flac、下首恢复用户档、服务端静默降档采信 | **ALL PASS** |
| 播放状态机 | player_flow_sim **P1-P8**:P7=VIP 钳顶(非会员用户档 SURROUND→请求档 EXHIGH/缓存键仍 1000_sky_c51/eff 回填;SVIP 直达);P8=fee 门禁(fee=1 非会员 NCM_ERR_VIP 零请求/缓存命中绕过/CVIP 放行/fee=8 放行);P1-P6 零回归 | **ALL PASS** |
| 滑窗/分页/缓存/封面/audiod | window_sim / pagination_sim / cache_evict_sim / cover_chain_sim / audiod_chain_sim(重编新 appstate.c/audio_out.c) | **全 PASS**(14/14 等) |
| 解码回归 | test_decode flac 44.1k/16 + 96k/24 + mp3(本批未动 decoder/audio_out,确认零回归) | **3/3 PASS** |
| mipsel 构建 | build_ncm.sh 自有代码 -Wall -Wextra | **0 warning**;text 962908(+8.4KB vs §19) |
| 宿主离屏 | build_ncm_host.sh + 6 页 MEOW_UI_DUMP | 全 rc=0;设置页布局核验(新行 sub/页脚生效) |
### 20.7 md5(本批产物 + 改动文件)
```
9d31f2241a62f618d7625a24f9f7a3a7 ncm # mipsel 静态 App(新;未部署,留主会话)
8a0a4ee03509c29ad7eb0ec9f61cd9db ncm_host # 宿主离屏调试二进制
2c1c459dbae23872557378477880d7a3 core/ncm_api.c
3d5fb722d5fdf5d9ac1f61e18450b586 core/ncm_api.h
e87f71c90175b67307d92d83d57b8886 core/ncm_common.h # +NCM_ERR_VIP(-15)
8e0b34b05f0063a4653236577e05cd47 core/ncm.h # 默认档注释
8bb15b7d9bd98f583761dab2260b98cd app/logic/account.c # 新:会员态缓存+刷新策略
3909d070d0290730739456ef0ab1e153 app/logic/appstate.c # 默认 EXHIGH/account_load/登出清理/level_key
f080c9f4823a5b59410445b5ccc4f191 app/play/player_manager.c # 钳顶+fee 门禁+eff_level
7f94bfbd6ce739a8a52273316ca2eb9d app/ui/settings.c # 八档菜单
baa6d5530764258ae9f93b6e4150e06e app/ui/player.c # 生效档小字+VIP 专享文案
bf776093956102989a43dfc778efdd01 app/ui/chrome.c # ncm_level_name/spec
771f351d82dbea46cc1d61c4bfd3bd41 app/ui/login.c # 登录后 vip 刷新
de6adb873dc426e4973f12866df21eeb app/ui/home.c # home worker 超期/到期刷新
6b6816096d1c26773f88a882f30fa65a app/ncm_app_internal.h # g_st.vip*/eff_level/声明
9f8a812f48d673514ae4b7145a6689c7 test/test_ncm_core.c # vip 解析+档位表用例
a5e76601554c8981457ab3a1987dcee2 bench/degrade_sim.c # 新:降级链 90 例矩阵
27b01f4bfd6199b62f6490367b47bd4b bench/player_flow_sim.c # P7/P8 + stub 更新
8af2179da8709b2b525e9744a887de79 build_ncm.sh # +account.c
ac5b29c40488f34e9220b14105bf63c2 build_ncm_host.sh # +account.c
```
### 20.8 诚实出入 / 遗留
- **真机未部署未验**(留主会话统一)。真机验收点:①登录后/超 1 天/到期三条触发路径
各只发一次 vip/info(日志 `account: vip refresh (<why>) -> vip=N`)+ account.json 落盘
②SVIP 档(jyeffect/sky/jymaster/sky_c51)按账号实测可用性逐级降 ③播放页「已降级至 xx」
小字 ④fee=1 非会员曲点播放直接「VIP 专享」且无 song_url 请求 ⑤新装默认档=极高 HQ。
- **sky 两档语义自研分配**(臻音全景声=裸 sky / 沉浸环绕声=sky+c51):BIZ_NOTES §2.1 命名
歧义无代码证据可消,真机若实测相反(如裸 sky 才是 5.1),只需对调 settings.c UI 表两项
的档位常量,零结构改动。Audio Vivid 若官方另有独立端点/参数则后续再补。
- 缓存命中的 flac 实体 eff 档推断可能高估(档内降级不可还原);VIP 档真实可用性以服务端
url 是否为空为准(门槛表只是省请求优化,服务端裁决权威)。
- vipType(login_status profile.vipType 10/11 口径)未用——BIZ_NOTES §1.2 标注非代码证据,
以 vip/info 双端点字段判定为准;真机若 vip/info 全挂可再加 profile.vipType 兜底。
- meow_sheet 无单项置灰(SDK 不在范围):未解锁档以「·未解锁」标注代替置灰。