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

+34
View File
@@ -103,3 +103,37 @@ void chrome_tick(void)
s_snack_until = 0;
}
}
/* ---- §20 音质档位显示名/规格(八档 UI 权威口径,NCM_CACHE_DESIGN §3;settings/player 共用)----
* 档位中文名以我方设计文档为准(BIZ_NOTES §2.1:CNMPlayer 标签只做 level 串对齐参考)。 */
const char *ncm_level_name(int lv)
{
switch (lv) {
case NCM_LEVEL_SURROUND: return "沉浸环绕声";
case NCM_LEVEL_SKY: return "臻音全景声";
case NCM_LEVEL_JYMASTER: return "超清母带";
case NCM_LEVEL_JYEFFECT: return "高清臻音";
case NCM_LEVEL_DOLBY: return "杜比全景声"; /* 映射保留,不进 UI 清单 */
case NCM_LEVEL_HIRES: return "Hi-Res";
case NCM_LEVEL_LOSSLESS: return "无损 SQ";
case NCM_LEVEL_EXHIGH: return "极高 HQ";
case NCM_LEVEL_HIGHER: return "较高"; /* 仅降级链中间档,不进 UI */
default: return "标准";
}
}
const char *ncm_level_spec(int lv)
{
switch (lv) {
case NCM_LEVEL_SURROUND: return "5.1 声道";
case NCM_LEVEL_SKY: return "Audio Vivid";
case NCM_LEVEL_JYMASTER: return "192kHz/24bit";
case NCM_LEVEL_JYEFFECT: return "96kHz/24bit";
case NCM_LEVEL_DOLBY: return "Dolby Atmos";
case NCM_LEVEL_HIRES: return "192kHz/24bit";
case NCM_LEVEL_LOSSLESS: return "48kHz/16bit";
case NCM_LEVEL_EXHIGH: return "≤320kbps";
case NCM_LEVEL_HIGHER: return "192kbps";
default: return "128kbps";
}
}
+2
View File
@@ -208,6 +208,8 @@ static void *home_worker(void *arg)
int rc;
if (!g_st.logged_in) rc = NCM_ERR_AUTH;
else rc = appstate_load_playlists();
if (rc == NCM_OK)
account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */
pthread_mutex_lock(&g_home_mu);
g_load_err = rc;
g_home_loading = 0;
+1
View File
@@ -154,6 +154,7 @@ static void *login_worker(void *arg)
meow_log(MEOW_LOG_INFO, "login: status_check rc=%d uid=%s nick=%s", src, uid, nick);
appstate_set_logged_in(src == NCM_OK ? uid : "", src == NCM_OK ? nick : "");
g_st.logged_in = 1;
account_vip_maybe_refresh(1); /* §20:登录后查一次会员态(本 worker 线程,不卡 UI) */
g_done = 1;
meow_log(MEOW_LOG_INFO, "login: -> home");
break;
+28 -3
View File
@@ -26,6 +26,7 @@ static int g_custom_disc, g_custom_dyn;
static int g_landscape;
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
static int g_last_dl, g_last_err; /* 缓冲进度粒度值 / 错误沿(§18) */
static int g_last_eff; /* 生效档沿(§20:降级标注出现/消失刷动态带) */
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
@@ -232,6 +233,22 @@ static void plr_draw_dyn(void *ctx)
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, th->surfaceVariant);
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
/* §20:实际生效音质档(与用户档不同时小字标注「已降级至 xx」;缓冲期未定不显示)。
* 位置:时间行中部(左右是时间戳,中央留白),竖/横屏同一条带。 */
{
int eff = player_manager_eff_level();
if (have && eff >= 0 && player_manager_state() != NCM_PC_STOPPED) {
char qb[72];
if (eff != g_st.level)
snprintf(qb, sizeof qb, "已降级至 %s", ncm_level_name(eff));
else
snprintf(qb, sizeof qb, "%s", ncm_level_name(eff));
int qcx = g_landscape ? (g_bar.x + g_bar.w / 2) : (r_cont.x + r_cont.w / 2);
uint32_t qc = (eff != g_st.level) ? pa->red : th->onSurfaceVariant;
meow_ui_text_center(qcx, g_times_y, qb, 18, qc);
}
}
int st = player_manager_state();
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
/* 暂停/停止(出错待重试)都画「播放」三角;播放/缓冲画暂停符 */
@@ -248,7 +265,8 @@ static void plr_draw_dyn(void *ctx)
int perr = player_manager_error();
if (perr != 0) {
/* §18:播放错误明示(旧版 err 无 UI 消费者 → 失败无声死等) */
const char *em = (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
const char *em = (perr == NCM_ERR_VIP) ? "VIP 专享,开通会员后可播放"
: (perr == NCM_ERR_NOTFOUND) ? "无法播放:无音源(VIP/版权)"
: (perr == NCM_ERR_AUTH) ? "登录已过期,请重新登录"
: "加载失败,请检查网络";
meow_ui_text_center(lcx, center_y - 20, em, 20, pa->red);
@@ -423,7 +441,14 @@ void player_tick(uint64_t now)
if (perr != g_last_err) {
g_last_err = perr;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
if (perr) ncm_snack("播放失败");
if (perr == NCM_ERR_VIP) ncm_snack("VIP 专享歌曲");
else if (perr) ncm_snack("播放失败");
}
/* §20:生效档变化(降级判定完成/切歌重置)刷动态带。 */
int eff = player_manager_eff_level();
if (eff != g_last_eff) {
g_last_eff = eff;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
}
}
@@ -440,7 +465,7 @@ static void plr_page_create(meow_view *v, void *ctx)
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
plr_draw_dyn, NULL);
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
g_last_dl = -1; g_last_err = 0;
g_last_dl = -1; g_last_err = 0; g_last_eff = -2; /* -2: 与 eff=-1(未定) 区分,首 tick 必刷 */
g_last_meta_id[0] = 0;
}
+43 -22
View File
@@ -1,8 +1,10 @@
/* ui/settings.c — 设置页(ACT_SETTINGS)。
*
* 行项:音质(Hi-res 默认,单曲不可用时 core ncm_api_song_url_auto 逐档降级)+ 缓存上限
* (默认 10GB,上限 100GB,支持预设与「自定义」数字输入)+ 清理缓存(显示当前占用)+
* 退出登录 + 关于/版本。设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
* 见 player_manager.c song_local_file)+ 缓存上限(默认 10GB,上限 100GB,支持预设与
* 「自定义」数字输入)+ 清理缓存(显示当前占用)+ 退出登录 + 关于/版本。
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
* ncm_store_read_cfg_int 同目录同键,ncm.cache_limit_gb/ncm.level)。超上限触发 ncm_cache LRU
* 淘汰(写缓存前后 + 手动调低上限时,见 player_manager.c/cover.c/appstate.c)。
* 空态/绘制用 custom 绘制物 + 手动行命中(仅 5 行,无需列表滚动)。
@@ -24,16 +26,12 @@ static uint64_t g_cache_used; /* 缓存占用字节(进设置页时
#define SETTINGS_ROWS 5
static const char *level_name(int lv)
{
switch (lv) {
case NCM_LEVEL_HIRES: return "Hi-Res";
case NCM_LEVEL_LOSSLESS:return "无损";
case NCM_LEVEL_EXHIGH: return "极高";
case NCM_LEVEL_HIGHER: return "较高";
default: return "标准";
}
}
/* 八档音质 UI 表(用户手机 App 口径为权威,NCM_CACHE_DESIGN §3):显示顺序高→低。
* HIGHER(较高)与 DOLBY(杜比全景声)不进 UI 清单(映射保留,见 ncm_api.h)。 */
static const int g_ui_levels[8] = {
NCM_LEVEL_SURROUND, NCM_LEVEL_JYMASTER, NCM_LEVEL_SKY, NCM_LEVEL_JYEFFECT,
NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS, NCM_LEVEL_EXHIGH, NCM_LEVEL_STANDARD,
};
/* 缓存占用字节 → 人类可读(整数运算,不用浮点/软浮点)。buf ≥ 24。 */
static void fmt_bytes(uint64_t b, char *buf, size_t sz)
@@ -80,7 +78,19 @@ static void settings_draw(void *ctx)
rows[2].title = "清理缓存"; rows[2].red = 0;
rows[3].title = "退出登录"; rows[3].red = 1;
rows[4].title = "关于网易云"; rows[4].red = 0;
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s", level_name(g_st.level));
{
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
int gate = ncm_api_level_gate(g_st.level);
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
int locked = (account_vip() < gate);
if (gate_s[0])
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) · 需 %s%s",
ncm_level_name(g_st.level), ncm_level_spec(g_st.level), gate_s,
locked ? ",将降级" : "");
else
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s)",
ncm_level_name(g_st.level), ncm_level_spec(g_st.level));
}
snprintf(rows[1].sub, sizeof rows[1].sub, "当前: %d GB · 上限 100", g_st.cache_limit_gb);
snprintf(rows[2].sub, sizeof rows[2].sub, "占用: %s", usetxt);
snprintf(rows[3].sub, sizeof rows[3].sub, "%s", g_st.nickname[0] ? g_st.nickname : "退出");
@@ -98,7 +108,7 @@ static void settings_draw(void *ctx)
/* 品牌红页脚小字。 */
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
meow_ui_text(x, ny, "Hi-Res/无损需黑胶会员,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
meow_ui_text(x, ny + 24, "网易云音乐 · MeowOS 移植版", 20, pa->red);
}
@@ -149,16 +159,27 @@ int settings_on_tap(int x, int y)
int row = row_at(x, y);
if (row < 0) return 0;
if (row == 0) { /* 音质:Top 档选择,单曲不可用时 core 逐档降级 */
static const char *lbls[5] = { "Hi-Res", "无损", "极高", "较高", "标准" };
static const int lvls[5] = { NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS,
NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER, NCM_LEVEL_STANDARD };
if (row == 0) { /* 音质:八档(§20;超权限档可选不硬拒,播放时钳顶+逐级降级) */
meow_sheet *s = meow_sheet_create("选择音质", 0);
for (int i = 0; i < 5; i++) meow_sheet_add_item(s, lbls[i]);
int vip = account_vip();
for (int i = 0; i < 8; i++) {
int lv = g_ui_levels[i];
int gate = ncm_api_level_gate(lv);
char lbl[96];
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
if (gate_s)
snprintf(lbl, sizeof lbl, "%s (%s) · %s%s", ncm_level_name(lv),
ncm_level_spec(lv), gate_s, (vip < gate) ? "·未解锁" : "");
else
snprintf(lbl, sizeof lbl, "%s (%s)", ncm_level_name(lv), ncm_level_spec(lv));
meow_sheet_add_item(s, lbl);
}
int op = meow_sheet_open(s);
meow_sheet_free(s);
if (op >= 0 && op < 5) {
g_st.level = lvls[op];
if (op >= 0 && op < 8) {
g_st.level = g_ui_levels[op];
player_manager_set_quality(g_st.level, g_st.cache_limit_gb);
appstate_save_settings();
meow_ui_invalidate(r_cont.x, r_cont.y, r_cont.w, r_cont.h);