feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
1 parent
c2ee7626a4
commit
3d70a3dd2c
176 files changed
+15507
-597
No files matched your search
@@ -18,6 +18,7 @@
|
||||
ncm_app_state_t g_st;
|
||||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
|
||||
.state = NCM_PC_STOPPED, .cur_song = -1 };
|
||||
static int g_reported_act = -1; /* 任务4 keep_awake:已上报 launcher 的顶层 activity(差分防重发) */
|
||||
volatile int g_app_quit = 0;
|
||||
volatile int g_net_ready = 0;
|
||||
int g_host_mock = 0;
|
||||
@@ -103,7 +104,15 @@ static void app_on_swipe(int dir, int dx, int dy)
|
||||
if (settings_is_active()) { meow_view_page_pop(); return; }
|
||||
if (playlist_is_active()) { meow_view_page_pop(); return; }
|
||||
if (search_is_active()) { meow_view_page_pop(); return; }
|
||||
/* home / login = 根 → 退出 */
|
||||
if (home_is_active()) {
|
||||
/* 任务2:home 根页左滑不再退出进程 → 请求 launcher 退到桌面/后台(进程+媒体会话保留)。
|
||||
* launcher M4 据 app.json background_audio:1 + 媒体会话 PLAYING 判保活;不保活才真终止。
|
||||
* 这里**不**置 g_app_quit / 不 meow_ui_request_exit——有歌在播则后台继续播(§M4)。 */
|
||||
meow_log(MEOW_LOG_INFO, "swipe home -> background (nav_exit)");
|
||||
ipc_media_nav_exit();
|
||||
return;
|
||||
}
|
||||
/* login 根(未登录):退出 */
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
return;
|
||||
@@ -139,6 +148,15 @@ static void app_on_swipe_progress(int x_start, int dx)
|
||||
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
/* 任务4 keep_awake:顶层 activity 变化 → 上报 launcher(MSG_ACT_FOCUS,差分一次)。
|
||||
* 页面切统一走 meow_view_page_push/pop/replace(更新 meow_ui_activity_active),此处差分上报:
|
||||
* 进播放页 active=ACT_PLAYER → ipc_act_report("player") → launcher fg_keep_awake 判常亮;
|
||||
* 退出播放页(弹回 home/playlist)→ 上报对应活动名(未声明 awake)→ 相机照常超时息屏。
|
||||
* 只在**当前顶层**变化时发一次(无 IPC 风暴;播放中 active 不变不重发)。 */
|
||||
{
|
||||
int act = meow_ui_activity_active();
|
||||
if (act != g_reported_act) { g_reported_act = act; ipc_act_report(act); }
|
||||
}
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
/* §NEW cookie 续期判会话失效 → 清登录态回登录页(worker 在后台续期发现 301/-460 时置标志;
|
||||
* 在此 UI 线程执行 logout+切页——meow_view_page_replace 非线程安全,只能 UI 线程做) */
|
||||
@@ -154,6 +172,16 @@ static void app_on_tick(void)
|
||||
int ai = autoplay_ui_consume();
|
||||
if (ai >= 0) player_play_song(ai);
|
||||
}
|
||||
/* 任务3:通知中心媒体卡片点卡片体(launcher 发 NCM_CTL_FOCUS)→ 回到播放页。
|
||||
* ipc_media 线程只置标志(跨线程 page 导航非线程安全),UI 线程消费后 push ACT_PLAYER;
|
||||
* 无在播曲则忽略(聚焦无意义)。 */
|
||||
if (ipc_media_focus_consume()) {
|
||||
if (player_manager_state() != NCM_PC_STOPPED &&
|
||||
meow_ui_activity_active() != ACT_PLAYER) {
|
||||
meow_log(MEOW_LOG_INFO, "ipc: focus -> player page");
|
||||
meow_view_page_push(ACT_PLAYER);
|
||||
}
|
||||
}
|
||||
if (login_is_active()) { login_tick(now); return; }
|
||||
if (player_is_active()) { player_tick(now); return; }
|
||||
if (search_is_active()) { search_tick(now); return; }
|
||||
|
||||
@@ -88,14 +88,14 @@ void account_load(void)
|
||||
int64_t last_refresh = ncm_json_get_i64_at(&j, ncm_json_path(&j, "last_refresh_ts"), &rc);
|
||||
if (rc != NCM_OK) last_refresh = 0;
|
||||
|
||||
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SVIP) vip = NCM_VIP_NONE;
|
||||
if (vip < NCM_VIP_NONE || vip > NCM_VIP_SUPER) vip = NCM_VIP_NONE;
|
||||
g_st.vip = (int)vip;
|
||||
g_st.vip_cvip_exp_ms = cvip_exp;
|
||||
g_st.vip_svip_exp_ms = svip_exp;
|
||||
g_st.vip_checked_at = checked;
|
||||
g_st.cookie_last_refresh_ts = last_refresh;
|
||||
/* 有效到期时间 = 当前档对应的到期字段 */
|
||||
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP) ? svip_exp
|
||||
/* 有效到期时间 = 当前档对应的到期字段(SUPER 无独立到期字段,沿黑胶 svip 场)。 */
|
||||
g_st.vip_expire_ms = (vip == NCM_VIP_SVIP || vip == NCM_VIP_SUPER) ? svip_exp
|
||||
: (vip == NCM_VIP_CVIP) ? cvip_exp : 0;
|
||||
meow_log(MEOW_LOG_INFO, "account: loaded vip=%d expire=%lld last_refresh_ts=%lld",
|
||||
g_st.vip, (long long)g_st.vip_expire_ms, (long long)last_refresh);
|
||||
@@ -129,9 +129,10 @@ int account_vip(void)
|
||||
int account_level_cap(void)
|
||||
{
|
||||
switch (account_vip()) {
|
||||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
case NCM_VIP_SVIP:
|
||||
case NCM_VIP_SUPER: return NCM_LEVEL_SURROUND;
|
||||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||||
default: return NCM_LEVEL_EXHIGH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ void account_vip_maybe_refresh(int force)
|
||||
g_st.vip = info.vip;
|
||||
g_st.vip_cvip_exp_ms = info.cvip_expire_ms;
|
||||
g_st.vip_svip_exp_ms = info.svip_expire_ms;
|
||||
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP) ? info.svip_expire_ms
|
||||
g_st.vip_expire_ms = (info.vip == NCM_VIP_SVIP || info.vip == NCM_VIP_SUPER) ? info.svip_expire_ms
|
||||
: (info.vip == NCM_VIP_CVIP) ? info.cvip_expire_ms : 0;
|
||||
g_st.vip_checked_at = now_s;
|
||||
account_persist(info.vip, info.cvip_expire_ms, info.svip_expire_ms, now_s);
|
||||
|
||||
@@ -278,8 +278,11 @@ static meow_img *download_cover(const char *url, ncm_cache_writer_t *w, size_t *
|
||||
/* 封面原始文件路径写入(供 launcher 媒体卡片读;兼容保留)。调用方须已持有 g_cm。
|
||||
* 从 cache 写者临时文件把已下载字节复制到 cover_current.jpg(早前版本在此内再 lock g_cm
|
||||
* → 非递归锁自锁死——cover worker 永久卡死、封面永不出现)。
|
||||
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。 */
|
||||
static void save_cover_raw(const char *path, size_t len)
|
||||
* §NEW 缓存分目录:与歌曲分池,cover_current.jpg 落 covers/(封面条目 song_cov* 也在 covers/)。
|
||||
* REPORT §封面混用:is_song=0(歌单封面,无曲目 id)时**只写 cover_current.jpg 镜像、不更新
|
||||
* g_cover_path**——g_cover_path 是上报 launcher 通知中心媒体卡片的「当前播放曲」封面指针;
|
||||
* 歌单页浏览加载歌单封面不应覆盖正在播放曲目的封面(用户真机:通知中心显示另一歌单封面)。 */
|
||||
static void save_cover_raw(const char *path, size_t len, int is_song)
|
||||
{
|
||||
if (!path || len == 0) return;
|
||||
const char *covdir = g_st.ctx.cache.covers_dir[0] ? g_st.ctx.cache.covers_dir
|
||||
@@ -301,17 +304,20 @@ static void save_cover_raw(const char *path, size_t len)
|
||||
}
|
||||
fclose(in);
|
||||
}
|
||||
ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
|
||||
if (is_song) ncm_strlcpy(g_cover_path, dst, sizeof g_cover_path);
|
||||
}
|
||||
}
|
||||
|
||||
#define COVER_RETRY_MAX 8 /* 连续失败放弃次数(UI 重进页/切歌会重新提交目标) */
|
||||
|
||||
/* 内存瘦身:封面解码后按显示上限降采样驻留。网易云全尺寸封面(如 1448×1448)解码成
|
||||
* RGBA = 8MB,而应用显示只需 ≤480px(播放页黑胶方图+歌词页模糊铺底均 ≤ 屏幕 480 宽)——
|
||||
* 缩到 480×480 = 0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀,最高优先级)。
|
||||
* 若已 ≤上限(小缩略图)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
|
||||
#define NCM_COVER_DECODE_MAX 480
|
||||
* RGBA = 8MB,而应用显示(歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到 720×720;封面页
|
||||
* 方图仅 304×304)——请求 param=640y640(见 ncm_app_internal.h)即 ≤640 边长,故此上限须 ≥ 640
|
||||
* 否则解码后又被降采样回 480、仍不够铺满 480 宽(480 图铺 480+ 显示照旧放大)。640×640 RGBA
|
||||
* ≈ 1.6MB 单张驻留(设备 RAM 仅 56MB,OOM 真机被杀;比 308 瘦身 8.28MB 仍省 6.7MB,单张 1.6MB
|
||||
* 在 32bit OOM 预算内——换封面仍先 free 旧再解码)。
|
||||
* 若已 ≤上限(小缩略图/无 param 全尺寸在 640 内)则原样保留。返回本函数处理后的图(可能替换/释放旧图)。 */
|
||||
#define NCM_COVER_DECODE_MAX 640
|
||||
static meow_img *cover_cap(meow_img *im)
|
||||
{
|
||||
if (!im) return NULL;
|
||||
@@ -362,7 +368,7 @@ static void *cover_worker(void *arg)
|
||||
img = download_cover(url, w, &blen, &drc, &dstatus, &dkind);
|
||||
if (img) {
|
||||
pthread_mutex_lock(&g_cm);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen);
|
||||
save_cover_raw(ncm_cache_writer_tmp_path(w), blen, song[0] ? 1 : 0);
|
||||
pthread_mutex_unlock(&g_cm);
|
||||
if (ncm_cache_commit(w) != 0) { ncm_cache_abort(w); }
|
||||
} else {
|
||||
@@ -452,6 +458,21 @@ static void *cover_worker(void *arg)
|
||||
if (g_cover_img) meow_img_free(g_cover_img);
|
||||
g_cover_img = img;
|
||||
ncm_strlcpy(g_cover_url, url, sizeof g_cover_url);
|
||||
/* 上报给 launcher 的封面路径 = 本目标专辑的缓存条目(per-URL 唯一)。
|
||||
* 旧码只在 save_cover_raw(下载路径)更新 g_cover_path;缓存命中路径从不更新 →
|
||||
* cover_current_path() 恒空 → MSG_META artwork 恒空 → 通知中心媒体卡拿不到封面路径
|
||||
* → 回退显示歌名。此处统一在「目标达成换入」时把 g_cover_path 指向已落盘的缓存条目
|
||||
* (下载 commit 后 / 缓存命中时均真实存在);commit 失败则保留 save_cover_raw 已写的
|
||||
* cover_current.jpg 镜像(仍在)。路径随专辑变化 → launcher 侧「按 artwork 文本缓存」
|
||||
* 的现有实现能随切歌重载,不残留旧封面。 */
|
||||
if (song[0] && g_st.ctx.cache.dir[0]) {
|
||||
char cpath[512];
|
||||
if (ncm_cache_path(&g_st.ctx.cache, key, ".jpg", cpath, sizeof cpath) == NCM_OK) {
|
||||
struct stat cs;
|
||||
if (stat(cpath, &cs) == 0 && cs.st_size > 0)
|
||||
ncm_strlcpy(g_cover_path, cpath, sizeof g_cover_path);
|
||||
}
|
||||
}
|
||||
g_gen++;
|
||||
g_tgt_url[0] = 0; /* 目标达成即清:worker 转怠速(修忙等死循环,§18) */
|
||||
} else {
|
||||
@@ -464,7 +485,8 @@ static void *cover_worker(void *arg)
|
||||
|
||||
/* UI 线程:提交目标封面 URL(无网络/无 URL 则忽略)。song_id:曲目 id(专辑封面死链时走
|
||||
* song/detail 兜底重取,见 REPORT §NEW 任务1);NULL/空=歌单封面(无兜底)。
|
||||
* 先 cover_norm_url 规范化(追加 ?param=300y300 缩略后缀,REPORT §NEW 任务2),使
|
||||
* 先 cover_norm_url 规范化(追加 ?param=640y640 缩略后缀,REPORT §NEW 任务2;640 适配歌词页
|
||||
* cover-fit 铺满 480×720,见 ncm_app_internal.h),使
|
||||
* g_tgt_url/g_cover_url/缓存键/下载 URL 走同一参数版本(小图省内存、命中高)。 */
|
||||
void cover_request(const char *url, const char *song_id)
|
||||
{
|
||||
|
||||
@@ -50,6 +50,41 @@ static void copy_lrc_text(char *dst, const char *src, size_t n, size_t max)
|
||||
dst[oi] = 0;
|
||||
}
|
||||
|
||||
/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后);
|
||||
* 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。
|
||||
* 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
|
||||
* 网易云 LRC 毫秒)——此前固定 ×10 把网易云 3 位毫秒当 2 位厘秒解析,时间戳放大 ~10 倍,
|
||||
* 歌词行按绝对时间激活滞后 ~4~10s(REPORT「英文歌歌词滚动慢」)。 */
|
||||
static int parse_lrc_tag(const char *p, long *out_ms, const char **next)
|
||||
{
|
||||
const char *q = p + 1; /* 跳过 '[' */
|
||||
int m = 0, md = 0, s = 0, sd = 0;
|
||||
while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; }
|
||||
if (md == 0 || *q != ':') goto notime;
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; }
|
||||
if (sd == 0) goto notime;
|
||||
long frac = 0; int fd = 0; long scale = 10;
|
||||
if (*q == '.') {
|
||||
q++;
|
||||
while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; }
|
||||
if (fd == 1) scale = 100;
|
||||
else if (fd == 2) scale = 10;
|
||||
else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */
|
||||
else scale = 0; /* 孤立 '.' 无数字:忽略小数 */
|
||||
}
|
||||
if (*q != ']') goto notime;
|
||||
*out_ms = (long)m * 60000L + (long)s * 1000L + frac * scale;
|
||||
*next = q + 1;
|
||||
return 1;
|
||||
notime:
|
||||
{
|
||||
const char *r = strchr(p, ']');
|
||||
*next = r ? r + 1 : NULL;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void lrc_parse(const char *lrc, int is_trans)
|
||||
{
|
||||
if (!lrc) return;
|
||||
@@ -62,19 +97,16 @@ static void lrc_parse(const char *lrc, int is_trans)
|
||||
int have = 0;
|
||||
long first_ms = 0;
|
||||
while (*p == '[') {
|
||||
int m = 0, s = 0, cs = 0;
|
||||
if (sscanf(p, "[%d:%d.%d]", &m, &s, &cs) >= 2) {
|
||||
long ms = (long)m * 60000L + (long)s * 1000L + (long)cs * 10L;
|
||||
long ms;
|
||||
const char *after;
|
||||
if (parse_lrc_tag(p, &ms, &after)) {
|
||||
if (!have) { first_ms = ms; have = 1; }
|
||||
/* 跳到本标签结尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) break;
|
||||
p = q + 1;
|
||||
p = after;
|
||||
} else {
|
||||
/* 非时间标签([ti:..] 等)——跳过到行尾 */
|
||||
const char *q = strchr(p, ']');
|
||||
if (!q) return;
|
||||
p = q + 1;
|
||||
if (!after) return; /* 无闭合 ']':残缺文件 */
|
||||
p = after;
|
||||
/* 若行内还有时间标签继续解析(极少),否则整行的正文以第一个文本为准 */
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,19 @@
|
||||
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
|
||||
|
||||
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
|
||||
* 缩略 JPEG——省内存(300×300≈20KB vs 全尺寸 1.34MB)+解 webp 三分型③+命中率高;已含 param= 的
|
||||
* URL 尊重原样)。共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
|
||||
* 同源,避免两处字符串漂移。 */
|
||||
* 缩略 JPEG——省内存 +解 webp 三分型③+命中率高;已含 param= 的 URL 尊重原样)。
|
||||
* 共享头声明:cover.c 与 bench/cover_chain_sim.c(URL 期望哈希须与实测 URL 同规则)
|
||||
* 同源,避免两处字符串漂移。
|
||||
* 值选择(封面分辨率适配歌词页铺满,REPORT:歌词页 cover-fit 铺满 480×720 全屏,方图实际渲染到
|
||||
* 720×720;旧默认 300y300 → 300 放大到 720 = 2.4× 最近邻放大 → 锐锯齿):
|
||||
* 640y640 → 歌词页 640→720 = 1.125× 轻微放大(铺满基本无锯齿),封面页方图 640→304 裁方降采样
|
||||
* 依旧锐利;解码 RGBA ≈ 640×640×4 ≈ 1.6MB 单张驻留(比 300 的 0.36MB +1.2MB anon,
|
||||
* 仍远低于 308 瘦身前 8.28MB;32bit OOM 已由 meow_cachegov 治理 page cache,封面 anon
|
||||
* 单张 1.6MB 在预算内——平衡清晰度与内存纪律)。
|
||||
* 注意 cover.c 的 NCM_COVER_DECODE_MAX 必须 ≥ 本参数边长,否则解码后仍被降采样回 480(不够铺
|
||||
* 满 480 宽 → 480 图铺 480+ 显示仍放大)。 */
|
||||
#define NCM_COVER_USE_PARAM 1
|
||||
#define NCM_COVER_PARAM "300y300"
|
||||
#define NCM_COVER_PARAM "640y640"
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
#define APP_W 480
|
||||
@@ -120,6 +128,9 @@ enum {
|
||||
NCM_CTL_NEXT = 4,
|
||||
NCM_CTL_SEEK = 5,
|
||||
NCM_CTL_TOGGLE = 6, /* 播放/暂停切换(物理键 164 恒发此;按当前态翻转) */
|
||||
NCM_CTL_FOCUS = 7, /* launcher 通知中心媒体卡片点卡片体 → 请求 App 回到播放页
|
||||
(任务3 §re-entry:launcher 侧点卡片体发本 cmd;App 置标志,
|
||||
UI 线程消费后导航 ACT_PLAYER,进程已后台保活则直接聚焦) */
|
||||
};
|
||||
|
||||
/* 跨线程播放运行时状态(main 只读展示;play 线程写,IPC 线程读)。 */
|
||||
@@ -142,6 +153,7 @@ typedef struct ncm_play_run {
|
||||
int underruns; /* 本曲 read_cb 追上下载的等待次数 */
|
||||
uint64_t underrun_ms; /* 本曲 read_cb 累计等待 ms */
|
||||
int stalled; /* 1=下载停滞(单次等待>1s,播放线程已对 audiod pause;UI 缓冲菊花) */
|
||||
int refaults; /* 本曲 DONTNEED 丢页重读 TF 次数(§2.3 取证) */
|
||||
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
|
||||
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
|
||||
ncm_song_t play_song;
|
||||
@@ -481,6 +493,9 @@ int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功;bi
|
||||
int audio_out_write(const void *samples, int frames); /* <0=错误/结束;样本位宽=open 的 bits */
|
||||
int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */
|
||||
void audio_out_close(void);
|
||||
int audio_out_sync_volume(void); /* 任务1:读 settings.conf 当前系统音量并下发到 audiod
|
||||
(MSG_AUDIO_VOLUME→写 DAC+持久化)。0=下发成功/-1=失败。
|
||||
audio_out_open SINK_AUDIOD 成功后已自动调;独立导出供宿主验证与复调。 */
|
||||
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
||||
int audio_out_buffered_ms(void);
|
||||
|
||||
@@ -515,6 +530,13 @@ int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t
|
||||
/* ipc_media:媒体会话上报/收指令 */
|
||||
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
|
||||
void ipc_media_stop(void);
|
||||
int ipc_media_nav_exit(void); /* 任务2:发 MSG_NAV{EXIT} 给 launcher 请求退后台(进程+媒体会话保留;
|
||||
launcher M4 据 background_audio+PLAYING 判保活)。0=发出/-1=失败。 */
|
||||
int ipc_media_focus_consume(void); /* 任务3:UI 线程消费「回播放页」标志(0=无;1=已消费一次) */
|
||||
int ipc_act_report(int act); /* 任务4 keep_awake:向 launcher 上报当前顶层 activity(MSG_ACT_FOCUS)。
|
||||
act=ACT_* 值,映射 "login/home/playlist/player/settings/search"(对齐
|
||||
app.json awake.<act> 键);launcher fg_keep_awake 依声明判常亮。
|
||||
0=发出/-1=无 socket 或写失败。 */
|
||||
|
||||
/* ---- 宿主测试面(app/test/test_ncm_app.c 驱动真实页面)---- */
|
||||
void app_host_set_mock(int mode); /* 0=无 1=home 2=playlist 3=player 4=settings 5=login */
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
* (无声排查的永久资产,勿省)。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include "meow_ipc.h" /* AUDIO_FMT_S16 */
|
||||
#include "meow_ipc.h" /* AUDIO_FMT_S16 / MSG_AUDIO_VOLUME / MEOW_IPC_HDR_LEN */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sound/asound.h>
|
||||
@@ -143,6 +145,111 @@ static int file_open(int rate, int ch, int bits)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 任务1:开播前把「系统当前音量」同步下发到 audiod */
|
||||
/* ============================================================ */
|
||||
/* 背景(用户实测 2026-09-02):未开始播放时音量不是系统设定值;按一下音量键才跳到设定值。
|
||||
* 根因= ncm 启动并打开 audiod/DAC 时,没有把系统当前音量下发到 audiod(meow_audiod 只在
|
||||
* MSG_AUDIO_VOLUME 收到时才写 DAC,流开/重配上不主动应用 settings 音量),导致 DAC 停在
|
||||
* 默认/上次残留音量;用户按音量键时 launcher/watchdog 那条音量链才更新并下发到 audiod。
|
||||
* 修法= SINK_AUDIOD open 成功后读 settings.conf 的 audio.volume,经 MSG_AUDIO_VOLUME 下发
|
||||
* (audiod 写 DAC numid=1/2 + 持久化 settings,同源同键)。语意:CS43198 寄存器=衰减量
|
||||
* (0 最响/255 静音),audiod dac_vol_set 负责反转与真静音(写 255),本处只传 pct。
|
||||
* 宿主验证:fake audiod socket 收帧断言 pct==系统设定(bench/vol_sync_sim.c)。 */
|
||||
|
||||
#define NCM_SETTINGS_CONF "/usr/data/meow/settings.conf" /* 与 audiod SETTINGS_CONF 同源 */
|
||||
#define NCM_AUDIO_SOCK_DEF "/usr/data/meow_audio.sock" /* 与 audiod / SDK 客户端同缺省 */
|
||||
#define NCM_VOL_DEFAULT 60 /* 与 audiod VOL_DEFAULT/SDK 缺省一致 */
|
||||
#define NCM_VOL_MAX 100
|
||||
|
||||
static const char *settings_conf_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_SETTINGS_FILE"); /* 宿主单测指向临时文件;设备缺省系统 conf */
|
||||
return (e && e[0]) ? e : NCM_SETTINGS_CONF;
|
||||
}
|
||||
|
||||
/* 读 settings.conf 的 audio.volume(扁平 "key = value";兼容无空格/多空格)。
|
||||
* 与 launcher meow_settings / SDK meow_ui_vol_read 同款解析。缺省 60;夹到 [0,100]。 */
|
||||
static int settings_read_volume(void)
|
||||
{
|
||||
static const char key[] = "audio.volume";
|
||||
const int kl = (int)sizeof key - 1;
|
||||
FILE *f = fopen(settings_conf_path(), "r");
|
||||
int vol = NCM_VOL_DEFAULT;
|
||||
if (!f) return vol;
|
||||
char line[160];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue; /* 非 key=value 行跳过 */
|
||||
char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
|
||||
int n = (int)(eq - k);
|
||||
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; /* key 右去空白 */
|
||||
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* 精确匹配 */
|
||||
char *v = eq + 1;
|
||||
while (*v == ' ' || *v == '\t') v++; /* value 左去空白 */
|
||||
int x = atoi(v);
|
||||
vol = (x < 0) ? 0 : (x > NCM_VOL_MAX ? NCM_VOL_MAX : x);
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
return vol;
|
||||
}
|
||||
|
||||
/* 把 pct(0-100) 下发到 audiod。独立连接(SDK 客户端连接私有不可复用);MSG_AUDIO_VOLUME
|
||||
* 不要求开流,audiod 侧写 DAC+持久化后 ACK(meow_audiod.c MSG_AUDIO_VOLUME)。失败仅日志,
|
||||
* 不影响播放本身(音量是次要正确性,非播放成败)。
|
||||
*
|
||||
* §2026-09-02 连接风暴根因:audiod_sync_volume 每次 audio_out_open 都新建一条 AF_UNIX
|
||||
* 短连接(socket→connect→write→close)。边下边播/切歌/seek 反复 open/close 时,audiod 端表现为
|
||||
* 「同一 fd=N 被反复 accept」(真机 1489 次 "io: client connected (fd=4)")。音量值实际只在
|
||||
* 用户按音量键时变化,且 audiod 收到已持久化(settings_set_volume),无需每次 open 重发——
|
||||
* 修法:每进程首次 open 同步一次;此后仅当 settings 音量**变化**才重发(同一连接数坍缩为
|
||||
* O(1),风暴消除)。失败不记 status(下次 open 仍会尝试,幂等)。 */
|
||||
static void audiod_sync_volume(int pct)
|
||||
{
|
||||
const char *sp = getenv("MEOW_AUDIO_SOCK");
|
||||
if (!sp || !sp[0]) sp = NCM_AUDIO_SOCK_DEF;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, sp, sizeof a.sun_path - 1);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return; }
|
||||
|
||||
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, len = sizeof(int32_t), req = 1;
|
||||
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
|
||||
memcpy(frame + 0, &magic, 4);
|
||||
memcpy(frame + 4, &ver, 2);
|
||||
memcpy(frame + 6, &type, 2);
|
||||
memcpy(frame + 8, &req, 4);
|
||||
memcpy(frame + 12, &len, 4);
|
||||
int32_t v32 = (int32_t)pct;
|
||||
memcpy(frame + 16, &v32, sizeof v32);
|
||||
if (write(fd, frame, sizeof frame) != (ssize_t)sizeof frame)
|
||||
meow_log(MEOW_LOG_WARN, "audio: sync volume write failed pct=%d", pct);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
|
||||
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
|
||||
static int g_vol_synced = 0;
|
||||
static int g_vol_last = -1;
|
||||
|
||||
int audio_out_sync_volume(void)
|
||||
{
|
||||
int pct = settings_read_volume();
|
||||
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
|
||||
g_vol_synced = 1;
|
||||
g_vol_last = pct;
|
||||
audiod_sync_volume(pct);
|
||||
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
|
||||
pct, settings_conf_path());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_out_open(int sample_rate, int channels, int bits)
|
||||
{
|
||||
g_rate = sample_rate; g_ch = channels; g_bits = bits;
|
||||
@@ -202,6 +309,9 @@ int audio_out_open(int sample_rate, int channels, int bits)
|
||||
else
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit",
|
||||
sid, sample_rate, channels, bits);
|
||||
/* 任务1:开播/打开音频会话时把系统当前音量同步下发到 audiod(DAC 停在默认/上次残留音量的根治)。
|
||||
* 幂等(同值重写无害);失败不阻断播放。 */
|
||||
audio_out_sync_volume();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static int g_quit;
|
||||
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
|
||||
|
||||
static const char *sock_path(void)
|
||||
{
|
||||
@@ -151,6 +152,11 @@ static void apply_playctl(const m_playctl_t *c)
|
||||
* (停止/缓冲)时 pause() 内部按态过滤,天然 no-op 安全。 */
|
||||
player_manager_pause(player_manager_state() == NCM_PC_PAUSED ? 0 : 1);
|
||||
break;
|
||||
case NCM_CTL_FOCUS:
|
||||
/* 任务3:通知中心媒体卡片点「卡片体」→ launcher 发本 cmd(需要 launcher 接线,见任务书):
|
||||
* 请求 App 回到播放页。只置标志(跨线程),UI 线程 app_on_tick 消费后导航(page nav 非线程安全)。 */
|
||||
g_req_focus_player = 1;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -201,6 +207,78 @@ static void *ipc_thread(void *arg)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 任务2:home 根页左滑「回桌面后台」——不再进程退出,改发 MSG_NAV{EXIT} 给 launcher。
|
||||
* launcher M4 据 app.json background_audio + 媒体会话 PLAYING(meow_media_app_playing)判
|
||||
* 保活(进程+音频流保留转后台,UI 走 §10.5 SDK present 自抑,非 SIGSTOP);否则才真终止。
|
||||
* 本函数**不**置 g_app_quit、**不**停线程——进程保留,媒体会话继续上报 PLAYING(任务2 ③)。
|
||||
* 独立连接发一帧(launcher ipc_dispatch MSG_NAV{EXIT} → meow_nav_request_exit,app_launcher.c:571)。
|
||||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||||
int ipc_media_nav_exit(void)
|
||||
{
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) {
|
||||
meow_log(MEOW_LOG_WARN, "ipc: nav_exit no launcher socket");
|
||||
return -1;
|
||||
}
|
||||
m_nav_t n;
|
||||
memset(&n, 0, sizeof n);
|
||||
ncm_strlcpy(n.app_id, "dev.player.ncm", sizeof n.app_id);
|
||||
n.nav_type = MEOW_NAV_EXIT;
|
||||
n.depth = 0;
|
||||
int rc = send_frame(fd, MSG_NAV, 9, &n, sizeof n);
|
||||
close(fd);
|
||||
meow_log(MEOW_LOG_INFO, "ipc: nav_exit sent rc=%d (request launcher background)", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 任务4 keep_awake:ACT_* 值 → launcher app.json awake/route 键用的活动名。
|
||||
* 命中的只有这些(ncm_app_internal.h ACT_* 枚举);未知/空栈(0) 返回 NULL(不起作用)。 */
|
||||
static const char *ncm_act_name(int act)
|
||||
{
|
||||
switch (act) {
|
||||
case ACT_LOGIN: return "login";
|
||||
case ACT_HOME: return "home";
|
||||
case ACT_PLAYLIST: return "playlist";
|
||||
case ACT_PLAYER: return "player";
|
||||
case ACT_SETTINGS: return "settings";
|
||||
case ACT_SEARCH: return "search";
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* 任务4 keep_awake:把当前顶层 activity 上报 launcher(MSG_ACT_FOCUS,独立连接发一帧,无需 HELLO)。
|
||||
* launcher util.c ipc_dispatch MSG_ACT_FOCUS → meow_fg_act_set(app,act);fg_keep_awake() 依
|
||||
* app.json awake.<act>=1 判定该 activity 是否常亮(超时不自动息屏)。无 socket/launcher 不跑 → 优雅跳过。
|
||||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||||
int ipc_act_report(int act)
|
||||
{
|
||||
const char *name = ncm_act_name(act);
|
||||
if (!name) return -1;
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) {
|
||||
meow_log(MEOW_LOG_WARN, "ipc: act_focus(%s) no launcher socket", name);
|
||||
return -1;
|
||||
}
|
||||
m_act_focus_t f;
|
||||
memset(&f, 0, sizeof f);
|
||||
ncm_strlcpy(f.app_id, "dev.player.ncm", sizeof f.app_id);
|
||||
ncm_strlcpy(f.activity, name, sizeof f.activity);
|
||||
int rc = send_frame(fd, MSG_ACT_FOCUS, 10, &f, sizeof f);
|
||||
close(fd);
|
||||
meow_log(MEOW_LOG_INFO, "ipc: act_focus act=%s rc=%d", name, rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 任务3:UI 线程消费「回到播放页」标志。返回 1=有未消费的聚焦请求(消费一次)。 */
|
||||
int ipc_media_focus_consume(void)
|
||||
{
|
||||
if (g_req_focus_player) {
|
||||
g_req_focus_player = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ipc_media_start(void)
|
||||
{
|
||||
if (g_thr_on) return 0;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* play/mem_watch.c — 边播边监控**取证**实现(接口见 mem_watch.h;agent 32bit 大文件 OOM 取证)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / meow_svc_perf;
|
||||
* 本模块退化为「RssAnon 取证」:低频读 /proc/self/status(优先 RssAnon,缺失回退 VmRSS),
|
||||
* 记录匿名内存峰值,**仅供日志**。不再驱动下载/预取降载(那由 meow_cachegov_download_paused
|
||||
* / prefetch_paused 查询)。**绝不打断解码/播放**。
|
||||
*
|
||||
* 阈值滞后(仅用于取证 level 标注):
|
||||
* kb >= CRITICAL → level=2;kb >= HIGH 且当前<2 → level=1;kb <= LOW → level=0;其它保持。
|
||||
* 本模块自含:只依赖 pthread + meow_log(SDK 日志),不依赖 core/app 其它 TU。
|
||||
*/
|
||||
#include "mem_watch.h"
|
||||
#include "../sdk/include/meow_ui_log.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* 默认阈值(设备缺省;宿主单测用 env/force 桩覆盖) */
|
||||
#define MEM_WATCH_PERIOD_MS 500
|
||||
#define MEM_WATCH_HIGH_KB (30 * 1024)
|
||||
#define MEM_WATCH_LOW_KB (20 * 1024)
|
||||
#define MEM_WATCH_CRITICAL_KB (40 * 1024)
|
||||
|
||||
struct mw_cfg {
|
||||
int period_ms;
|
||||
long high_kb;
|
||||
long low_kb;
|
||||
long critical_kb;
|
||||
};
|
||||
|
||||
static struct mw_cfg g_cfg = {
|
||||
.period_ms = MEM_WATCH_PERIOD_MS,
|
||||
.high_kb = MEM_WATCH_HIGH_KB,
|
||||
.low_kb = MEM_WATCH_LOW_KB,
|
||||
.critical_kb = MEM_WATCH_CRITICAL_KB,
|
||||
};
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
static volatile int g_quit;
|
||||
static long g_rss_anon_kb; /* 最近一次采样(KB;-1=不可读) */
|
||||
static long g_peak_song_kb; /* 本曲匿名峰值(song_begin 清零) */
|
||||
static int g_level; /* NCM_MEM_LVL_*(仅取证日志) */
|
||||
static long g_forced_kb = -1; /* 宿主桩 ≥0 时替代真读 */
|
||||
|
||||
/* env 覆盖阈值:非法值回落默认。 */
|
||||
static int env_int(const char *k, int def)
|
||||
{
|
||||
const char *e = getenv(k);
|
||||
if (!e || !e[0]) return def;
|
||||
int v = atoi(e);
|
||||
return v > 0 ? v : def;
|
||||
}
|
||||
static long env_long(const char *k, long def)
|
||||
{
|
||||
const char *e = getenv(k);
|
||||
if (!e || !e[0]) return def;
|
||||
long v = atol(e);
|
||||
return v > 0 ? v : def;
|
||||
}
|
||||
|
||||
/* 读匿名 RSS(KB)。优先 RssAnon;缺则回退 VmRSS;都无 → -1。 */
|
||||
static long read_rss_kb(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long forced = g_forced_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (forced >= 0) return forced;
|
||||
|
||||
FILE *fp = fopen("/proc/self/status", "r");
|
||||
if (!fp) return -1;
|
||||
char line[256];
|
||||
long anon = -1, rss = -1;
|
||||
while (fgets(line, sizeof line, fp))
|
||||
if (!strncmp(line, "RssAnon:", 8)) anon = atol(line + 8);
|
||||
else if (rss < 0 && !strncmp(line, "VmRSS:", 6)) rss = atol(line + 6);
|
||||
fclose(fp);
|
||||
return anon >= 0 ? anon : rss;
|
||||
}
|
||||
|
||||
static const char *level_str(int lvl)
|
||||
{
|
||||
switch (lvl) {
|
||||
case NCM_MEM_LVL_HIGH: return "high";
|
||||
case NCM_MEM_LVL_CRITICAL: return "critical";
|
||||
default: return "normal";
|
||||
}
|
||||
}
|
||||
|
||||
static void *watch_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_quit) {
|
||||
long kb = read_rss_kb();
|
||||
int lvl;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_rss_anon_kb = kb;
|
||||
if (kb > g_peak_song_kb) g_peak_song_kb = kb;
|
||||
long hi = g_cfg.high_kb, lo = g_cfg.low_kb, cr = g_cfg.critical_kb;
|
||||
/* 滞后三档(仅取证 level 标注) */
|
||||
if (kb >= cr) lvl = NCM_MEM_LVL_CRITICAL;
|
||||
else if (kb >= hi) lvl = (g_level == NCM_MEM_LVL_CRITICAL) ? NCM_MEM_LVL_CRITICAL : NCM_MEM_LVL_HIGH;
|
||||
else if (kb <= lo) lvl = NCM_MEM_LVL_NORMAL;
|
||||
else lvl = g_level; /* 滞后带:保持 */
|
||||
if (lvl != g_level) {
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"[mem] rss_anon=%ldKB peak=%ldKB level=%s (H=%ldK/L=%ldK/C=%ldK)",
|
||||
kb, g_peak_song_kb, level_str(lvl), hi, lo, cr);
|
||||
}
|
||||
g_level = lvl;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
usleep((useconds_t)g_cfg.period_ms * 1000u);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int mem_watch_start(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_thr_on) { pthread_mutex_unlock(&g_mu); return 0; }
|
||||
/* 开关(默认开;NCM_MEM_WATCH_ENABLED=0/off 关闭监控,行为退化为旧版零侵入) */
|
||||
const char *en = getenv("NCM_MEM_WATCH_ENABLED");
|
||||
if (en && (!en[0] || !strcmp(en, "0") || !strcmp(en, "off") || !strcmp(en, "false"))) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] watch disabled (NCM_MEM_WATCH_ENABLED=%s)", en);
|
||||
return 0;
|
||||
}
|
||||
/* 阈值 env 覆盖(宿主单测) */
|
||||
g_cfg.period_ms = env_int("NCM_MEM_WATCH_PERIOD_MS", MEM_WATCH_PERIOD_MS);
|
||||
g_cfg.high_kb = env_long("NCM_MEM_WATCH_HIGH_KB", MEM_WATCH_HIGH_KB);
|
||||
g_cfg.low_kb = env_long("NCM_MEM_WATCH_LOW_KB", MEM_WATCH_LOW_KB);
|
||||
g_cfg.critical_kb = env_long("NCM_MEM_WATCH_CRITICAL_KB", MEM_WATCH_CRITICAL_KB);
|
||||
g_quit = 0;
|
||||
pthread_attr_t a;
|
||||
pthread_attr_init(&a);
|
||||
pthread_attr_setstacksize(&a, (size_t)(128 * 1024)); /* 同 ncm_thread_create 128KB(内存瘦身,OOM 修复) */
|
||||
int rc = pthread_create(&g_thr, &a, watch_thread, NULL);
|
||||
pthread_attr_destroy(&a);
|
||||
if (rc != 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
g_thr_on = 1;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"[mem] watch started period=%dms H=%ldK L=%ldK C=%ldK (RssAnon 优先)",
|
||||
g_cfg.period_ms, g_cfg.high_kb, g_cfg.low_kb, g_cfg.critical_kb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mem_watch_stop(void)
|
||||
{
|
||||
g_quit = 1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_thr_on) { g_thr_on = 0; pthread_detach(g_thr); }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
int mem_watch_level(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int v = g_level;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return v;
|
||||
}
|
||||
|
||||
long mem_watch_rss_anon_kb(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long v = g_rss_anon_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return v;
|
||||
}
|
||||
|
||||
void mem_watch_song_begin(void)
|
||||
{
|
||||
long kb = read_rss_kb(); /* 起播即采样当前内存,本曲峰值从当前起算 */
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_peak_song_kb = kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] song begin rss_anon=%ldKB", kb);
|
||||
}
|
||||
|
||||
/* 播完/切歌释放钩子之后调用:落本曲峰值 + 重置计数。 */
|
||||
void mem_watch_song_end(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
long peak = g_peak_song_kb;
|
||||
g_peak_song_kb = g_rss_anon_kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
meow_log(MEOW_LOG_INFO, "[mem] song end peak_anon=%ldKB (released)", peak);
|
||||
}
|
||||
|
||||
void mem_watch_set_forced_kb(long kb)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_forced_kb = kb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/* play/mem_watch.h — 边播边监控**取证**(agent 32bit 大文件 OOM 取证;OOM_PAGECACHE_GOVERNANCE)。
|
||||
*
|
||||
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
|
||||
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / 系统服务
|
||||
* meow_svc_perf;本模块退化为「RssAnon 取样取证」:一个轻量线程(ncm_thread_create 同款
|
||||
* 128KB 栈)低频(默认 500ms)读 /proc/self/status 的 RssAnon(缺失回退 VmRSS),记录匿名
|
||||
* 内存峰值(RssAnon 贴近「应用自有不可回收内存」,是 OOM kill 高危口径),**仅供日志/取证**,
|
||||
* 不再驱动下载/预取降载(那由 meow_cachegov_download_paused/prefetch_paused 查询)。
|
||||
*
|
||||
* 调度器结合释放钩子:mem_watch_song_begin/end 在 run_song 的起播/收尾(song_src_dispose +
|
||||
* ncm_dec_close + audio_out_close,即「彻底释放」钩子)处调用,重置本曲峰值计数并落日志
|
||||
* (峰值 RSS 即 RssAnon/RssFile 分离取证锚点;Cached/Dirty 侧取证归 svc_perf/meow_cachegov)。
|
||||
*
|
||||
* 阈值/开关可通过环境变量覆盖(宿主单测确定性驱动):NCM_MEM_WATCH_ENABLED /
|
||||
* NCM_MEM_WATCH_PERIOD_MS / NCM_MEM_WATCH_HIGH_KB / NCM_MEM_WATCH_LOW_KB / NCM_MEM_WATCH_CRITICAL_KB。
|
||||
* 宿主测试桩:mem_watch_set_forced_kb() 注入读值(>=0 时监控线程不再读真 proc,仅测取证逻辑)。
|
||||
*/
|
||||
#ifndef NCM_MEM_WATCH_H
|
||||
#define NCM_MEM_WATCH_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 内存压力档(仅取证日志用;不再驱动降载) */
|
||||
enum {
|
||||
NCM_MEM_LVL_NORMAL = 0, /* < HIGH */
|
||||
NCM_MEM_LVL_HIGH = 1, /* ≥ HIGH */
|
||||
NCM_MEM_LVL_CRITICAL = 2, /* ≥ CRITICAL */
|
||||
};
|
||||
|
||||
/* 启动监控线程(幂等;ncm_thread_create 128KB 栈)。返回 0=ok / -1=线程建失败。 */
|
||||
int mem_watch_start(void);
|
||||
/* 停止(置退出标志;线程 detach,不 join——同 ipc_media 模式)。 */
|
||||
void mem_watch_stop(void);
|
||||
|
||||
/* 当前压力档(0/1/2;仅取证日志)。 */
|
||||
int mem_watch_level(void);
|
||||
/* 最近一次采样的匿名 RSS(KB);-1=不可读。 */
|
||||
long mem_watch_rss_anon_kb(void);
|
||||
|
||||
/* ---- 调度器:与播放释放钩子联动 ---- */
|
||||
/* 起播一首曲时调用:本曲峰值计数清零,开始记录本曲内存画像。 */
|
||||
void mem_watch_song_begin(void);
|
||||
/* 播完/切歌、即「彻底释放」钩子(decoder 关+下载线程 join+源关+.part 收尾)执行后调用:
|
||||
* 落本曲峰值日志 + 重置计数,给下一首让出统计(内存本身已由释放钩子归还)。 */
|
||||
void mem_watch_song_end(void);
|
||||
|
||||
/* ---- 宿主测试桩(设备永不调用;≥0 时监控线程用注入值替代真 /proc 读)---- */
|
||||
void mem_watch_set_forced_kb(long kb);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* NCM_MEM_WATCH_H */
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include "decoder.h"
|
||||
#include "stream_src.h"
|
||||
#include "mem_watch.h"
|
||||
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on;
|
||||
@@ -43,6 +44,10 @@ static char g_path[512]; /* 本地音频路径(缓存,mp3/flac
|
||||
static int64_t g_file_bytes;
|
||||
static int g_level; /* 音质档(来自设置) */
|
||||
static int g_cache_gb;
|
||||
/* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档
|
||||
* (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的
|
||||
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
|
||||
static int g_pending_replay;
|
||||
|
||||
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
||||
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
||||
@@ -90,12 +95,25 @@ void player_manager_set_quality(int level, int cache_gb)
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.err = 0;
|
||||
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
|
||||
g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
void player_manager_play(int song_idx)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
/* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播:
|
||||
* 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。
|
||||
* 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。
|
||||
* 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */
|
||||
if (song_idx == g_play.cur_song && !g_pending_replay &&
|
||||
(g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) {
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart",
|
||||
song_idx, g_play.state);
|
||||
return;
|
||||
}
|
||||
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
|
||||
g_play.cur_song = song_idx;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
@@ -108,6 +126,7 @@ void player_manager_play(int song_idx)
|
||||
g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */
|
||||
g_play.underrun_ms = 0;
|
||||
g_play.stalled = 0;
|
||||
g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
|
||||
@@ -135,6 +154,7 @@ void player_manager_next(int dir)
|
||||
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
|
||||
if (c >= cnt) c = cnt - 1;
|
||||
if (c != g_play.cur_song) {
|
||||
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
|
||||
g_play.cur_song = c;
|
||||
g_play.cmd = 0;
|
||||
g_play.state = NCM_PC_BUFFER;
|
||||
@@ -147,6 +167,7 @@ void player_manager_next(int dir)
|
||||
g_play.underruns = 0;
|
||||
g_play.underrun_ms = 0;
|
||||
g_play.stalled = 0;
|
||||
g_play.refaults = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
}
|
||||
@@ -154,6 +175,7 @@ void player_manager_next(int dir)
|
||||
void player_manager_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_pending_replay = 0; /* 停止:清「切档重播」待办 */
|
||||
g_play.cur_song = -1;
|
||||
g_play.state = NCM_PC_STOPPED;
|
||||
g_play.pos_ms = 0;
|
||||
@@ -189,8 +211,19 @@ typedef struct {
|
||||
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
|
||||
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
|
||||
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
|
||||
/* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */
|
||||
char gov_path[512];
|
||||
/* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */
|
||||
char gov_final_path[512];
|
||||
} song_src_t;
|
||||
|
||||
/* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针
|
||||
* (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */
|
||||
static uint64_t gov_read_pos(void *ctx)
|
||||
{
|
||||
return stream_src_read_pos((stream_src_t *)ctx);
|
||||
}
|
||||
|
||||
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
|
||||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
|
||||
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
|
||||
@@ -201,6 +234,11 @@ static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
song_src_t *ss = (song_src_t *)ctx;
|
||||
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
|
||||
/* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停
|
||||
* OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程
|
||||
* (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */
|
||||
while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src))
|
||||
usleep(50 * 1000);
|
||||
if (ss->first_body) {
|
||||
ss->first_body = 0;
|
||||
int st = ss->st_ptr ? *ss->st_ptr : 0;
|
||||
@@ -295,6 +333,15 @@ static void song_src_dispose(song_src_t *ss)
|
||||
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
|
||||
}
|
||||
|
||||
/* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做
|
||||
* L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */
|
||||
static void song_gov_cleanup(song_src_t *ss)
|
||||
{
|
||||
if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path);
|
||||
if (ss->dl_rc == NCM_OK && ss->gov_final_path[0])
|
||||
meow_cachegov_drop_finished(ss->gov_final_path);
|
||||
}
|
||||
|
||||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
|
||||
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
|
||||
@@ -587,6 +634,7 @@ static void run_song(int idx)
|
||||
g_play.play_song = s;
|
||||
g_play.play_song_valid = 1;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */
|
||||
|
||||
song_src_t ss;
|
||||
int64_t file_bytes = 0;
|
||||
@@ -602,11 +650,22 @@ static void run_song(int idx)
|
||||
}
|
||||
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */
|
||||
mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */
|
||||
return;
|
||||
}
|
||||
ncm_strlcpy(g_path, path, sizeof g_path);
|
||||
g_file_bytes = file_bytes;
|
||||
|
||||
/* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读
|
||||
* 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part
|
||||
* (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */
|
||||
ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path);
|
||||
if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path);
|
||||
else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path);
|
||||
meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0),
|
||||
gov_read_pos, ss.src);
|
||||
|
||||
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
|
||||
|
||||
/* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb
|
||||
@@ -616,6 +675,8 @@ static void run_song(int idx)
|
||||
int64_t total_frames = 0;
|
||||
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss);
|
||||
mem_watch_song_end(); /* 释放钩子 */
|
||||
if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */
|
||||
set_err(NCM_ERR_IO, "decode open failed"); return;
|
||||
}
|
||||
@@ -623,7 +684,8 @@ static void run_song(int idx)
|
||||
|
||||
/* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */
|
||||
if (audio_out_open(rate, ch, bits) != 0) {
|
||||
ncm_dec_close(dec); song_src_dispose(&ss);
|
||||
ncm_dec_close(dec); song_src_dispose(&ss); song_gov_cleanup(&ss);
|
||||
mem_watch_song_end(); /* 释放钩子 */
|
||||
set_err(NCM_ERR_IO, "audio open failed"); return;
|
||||
}
|
||||
|
||||
@@ -645,6 +707,7 @@ static void run_song(int idx)
|
||||
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
|
||||
int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */
|
||||
int underruns_seen = 0;
|
||||
int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */
|
||||
|
||||
for (;;) {
|
||||
if (g_app_quit) break;
|
||||
@@ -751,6 +814,18 @@ static void run_song(int idx)
|
||||
idx, u, (unsigned long long)wm);
|
||||
}
|
||||
}
|
||||
/* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */
|
||||
{
|
||||
int rf = stream_src_refaults(ss.src);
|
||||
if (rf != refaults_seen) {
|
||||
refaults_seen = rf;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.refaults = rf;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf);
|
||||
}
|
||||
meow_cachegov_tick();
|
||||
}
|
||||
/* 更新进度(~200ms 节流)*/
|
||||
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
@@ -763,6 +838,9 @@ static void run_song(int idx)
|
||||
ncm_dec_close(dec);
|
||||
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
|
||||
song_src_dispose(&ss);
|
||||
song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */
|
||||
mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程
|
||||
* join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */
|
||||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
|
||||
}
|
||||
|
||||
@@ -835,7 +913,9 @@ static void *lyric_worker(void *arg)
|
||||
}
|
||||
if (!id[0]) continue;
|
||||
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
|
||||
|
||||
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
|
||||
* (A 回退硬暂停 OR svc_perf C Cached 降载),
|
||||
* 不置 last_id → 降载恢复后下轮再拉。 */
|
||||
ncm_strlcpy(last_id, id, sizeof last_id);
|
||||
lyric_clear();
|
||||
pthread_mutex_lock(&g_lyrmu);
|
||||
@@ -898,6 +978,7 @@ int player_manager_start(void)
|
||||
g_cache_gb = g_st.cache_limit_gb;
|
||||
pthread_mutex_init(&g_play.mu, NULL);
|
||||
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
||||
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
|
||||
if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1;
|
||||
g_thr_on = 1;
|
||||
return 0;
|
||||
@@ -908,6 +989,7 @@ void player_manager_stop_thread(void)
|
||||
g_want_run = 0;
|
||||
g_app_quit = 1;
|
||||
audio_out_close(); /* 打断阻塞写 */
|
||||
mem_watch_stop(); /* 停监控线程 */
|
||||
if (g_thr_on) {
|
||||
pthread_join(g_thr, NULL);
|
||||
g_thr_on = 0;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define REBUFFER_AFTER_MS 500 /* 单次调用累计等待超此值后启用再缓冲迟滞 */
|
||||
#define REBUFFER_MIN (64 * 1024) /* 迟滞放行门槛(≈1.6s@320kbps) */
|
||||
#define STALL_MS 1000 /* 单次调用等待超此值 → 置 stalled(播放线程 pause 联动) */
|
||||
#define STREAM_SRC_REFAULT_MS 50 /* pread 慢于此阈且数据已在盘上 → 计一次 refault(§2.3) */
|
||||
|
||||
struct stream_src {
|
||||
int fd; /* 只读 fd(pread;写端另有自己的写 fd) */
|
||||
@@ -36,8 +37,16 @@ struct stream_src {
|
||||
uint64_t wait_ms_total;
|
||||
int underruns;
|
||||
int stalled;
|
||||
int refaults; /* 数据在盘上但 pread 慢(>50ms)= DONTNEED 丢页重读 TF(§2.3) */
|
||||
};
|
||||
|
||||
/* SDK meow_cachegov 提供的 refault 上报(strong 时覆盖弱符号;未链 SDK 时为空指针,调用需判空)。 */
|
||||
#if defined(__GNUC__)
|
||||
extern void meow_cachegov_note_refault(void) __attribute__((weak));
|
||||
#else
|
||||
extern void meow_cachegov_note_refault(void);
|
||||
#endif
|
||||
|
||||
static int64_t mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
@@ -182,9 +191,18 @@ static size_t src_read(void *buf, size_t size, void *ud)
|
||||
if (avail > 0 && !gated) {
|
||||
size_t n = (size - done) < (size_t)avail ? (size - done) : (size_t)avail;
|
||||
uint64_t pos = s->read_pos;
|
||||
int had_data = (pos < s->written); /* 数据已在盘上(放锁前采样;勿在放锁后读 written) */
|
||||
pthread_mutex_unlock(&s->mu); /* pread 期间放锁,不阻塞写端 broadcast */
|
||||
int64_t t0 = mono_ms();
|
||||
ssize_t r = pread(s->fd, (char *)buf + done, n, (off_t)pos);
|
||||
int64_t pread_ms = mono_ms() - t0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
if (r > 0 && had_data && pread_ms > STREAM_SRC_REFAULT_MS) {
|
||||
/* 数据在盘上但 pread 慢 = DONTNEED 丢页后重读 TF(underrun 确认是 refault,
|
||||
* 非网络等待)。上报 SDK meow_cachegov(弱符号,未链时为空指针需判空)。 */
|
||||
s->refaults++;
|
||||
if (meow_cachegov_note_refault) meow_cachegov_note_refault();
|
||||
}
|
||||
if (r <= 0) { /* 盘 IO 错:按中断收尾(真 EOF 语义) */
|
||||
if (!s->failed) s->failed = -3; /* NCM_ERR_IO(本层不依赖 core 头) */
|
||||
s->final = s->written ? s->written : 1;
|
||||
@@ -290,6 +308,24 @@ uint64_t stream_src_written(stream_src_t *s)
|
||||
return w;
|
||||
}
|
||||
|
||||
int stream_src_refaults(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
int r = s->refaults;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return r;
|
||||
}
|
||||
|
||||
uint64_t stream_src_read_pos(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
pthread_mutex_lock(&s->mu);
|
||||
uint64_t p = s->read_pos;
|
||||
pthread_mutex_unlock(&s->mu);
|
||||
return p;
|
||||
}
|
||||
|
||||
uint64_t stream_src_final(stream_src_t *s)
|
||||
{
|
||||
if (!s) return 1;
|
||||
|
||||
@@ -72,6 +72,15 @@ void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms);
|
||||
* 取走即清零。 */
|
||||
int stream_src_take_stalled(stream_src_t *s);
|
||||
|
||||
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE §2.3:underrun/refault 检测)----
|
||||
* refault = pread 处「数据已在盘上(written>read_pos)但读出耗时 >50ms」——DONTNEED 丢页后
|
||||
* 重读 TF 的信号,区别于网络 underrun(avail==0 等待)。SDK meow_cachegov 据此做 A 回退。
|
||||
* 播放线程读 refaults 落 g_play 取证;回调由 SDK(meow_cachegov_note_refault)提供,
|
||||
* 未链 SDK 时弱符号为空(host sim 兼容)。 */
|
||||
int stream_src_refaults(stream_src_t *s);
|
||||
/* 当前解码逻辑读指针(stream_src_seek_cb 改写)。 */
|
||||
uint64_t stream_src_read_pos(stream_src_t *s);
|
||||
|
||||
uint64_t stream_src_written(stream_src_t *s);
|
||||
uint64_t stream_src_final(stream_src_t *s); /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
|
||||
int stream_src_failed(stream_src_t *s); /* 0=无错误;<0=下载/IO 错误码 */
|
||||
|
||||
@@ -266,10 +266,11 @@ int main(int argc, char **argv)
|
||||
player_touch_move(340, 200); /* 右移 100px */
|
||||
player_on_swipe(MEOW_SWIPE_RIGHT, 100); /* SDK 中部右滑 → SWIPE_RIGHT */
|
||||
int p2 = player_page();
|
||||
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,504)=进度条行 */
|
||||
player_touch_down(240, 504);
|
||||
player_touch_move(140, 504);
|
||||
player_on_tap(140, 504); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
|
||||
/* 起点在进度条上不锁存(防拖进度条误判翻页);(240,484)=进度条行(§22 时间行下移后
|
||||
* g_bar.y=484,时间行在 500——拖时间行会误判翻页,故取进度条行本身) */
|
||||
player_touch_down(240, 484);
|
||||
player_touch_move(140, 484);
|
||||
player_on_tap(140, 484); /* 拖进度条抬手 → 应仍为封面页(seek 而非翻页) */
|
||||
int latched_change = player_page();
|
||||
int ok = (p0 == 0 && p1 == 1 && p2 == 0 && latched_change == 0);
|
||||
printf("gesture: p0=%d p1(左滑)=%d p2(右滑)=%d 省电锁存=%d -> %s\n",
|
||||
|
||||
@@ -120,6 +120,27 @@ static void home_draw(void *ctx)
|
||||
meow_ui_text(pc.x + asz + 28, pc.y + 6, g_st.nickname[0] ? g_st.nickname : "我的歌单",
|
||||
th->font_body, th->onSurface);
|
||||
meow_ui_text(pc.x + asz + 28, pc.y + 6 + th->font_body + 4, idtxt, th->font_body, th->onSurfaceVariant);
|
||||
/* §22 会员等级小字:account_vip() 枚举(用户语义 0=无 / 1=畅听会员 / 2=黑胶 VIP /
|
||||
* 3=超级会员);昵称右侧画小红徽章(red_ctr 底 + 红字),无会员则不画。 */
|
||||
{
|
||||
int vip = account_vip();
|
||||
if (vip != NCM_VIP_NONE) {
|
||||
const char *vip_lab = (vip == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (vip == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (vip == NCM_VIP_SUPER) ? "超级会员" : "";
|
||||
const char *nm = g_st.nickname[0] ? g_st.nickname : "我的歌单";
|
||||
int nmw = meow_ui_text_width(nm, th->font_body);
|
||||
int bs = th->font_body - 2; /* 徽章字号略小 */
|
||||
if (bs < 16) bs = 16;
|
||||
int bw = meow_ui_text_width(vip_lab, bs) + 12;
|
||||
int bx = pc.x + asz + 28 + nmw + 8;
|
||||
int by = pc.y + 6 + (th->font_body - (bs + 4)) / 2;
|
||||
if (bx + bw <= pc.x + pc.w - 24) { /* 不越卡片右缘(避开“>”) */
|
||||
ncm_fill_round(bx, by, bw, bs + 4, 6, pa->red_ctr);
|
||||
meow_ui_text_center(bx + bw / 2, by + 2, vip_lab, bs, pa->red);
|
||||
}
|
||||
}
|
||||
}
|
||||
meow_ui_text_right(pc.x + pc.w - 16, pc.y + (pc.h - 28) / 2, ">", 28, th->onSurfaceVariant);
|
||||
|
||||
/* 双页签(g_tabs=meow_tabbar widget 建)——红下划线/红选中字叠层。 */
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* - 封面页:方形大图+圆角(cover.c LRU,无旋转动画)+ 进度条(可点 seek)+ MD3 矢量控制钮
|
||||
* (prev/play-pause/next,ncm_icons.h 32x32 单通道 alpha,参照 sb_icons.h 范式)
|
||||
* + 元信息 + 实际音质档 + 页面指示点;缓冲/错误态沿用(「缓冲中…N%」/中文错误条+重试)。
|
||||
* - 歌词页:LRC 滚动(当前行高亮居中,上下行渐隐)+ 点行跳转;默认=封面模糊置底,
|
||||
* 省电=封面背景+黑色半透明叠加层(设置 ncm.player_bg_mode: default/power_save)。
|
||||
* - 歌词页:LRC 滚动(活动行纯白偏上锚定,上下行逐级灰阶)+ 点行跳转;默认=封面模糊置底
|
||||
* (低透明非纯黑中性叠加层,需求1 加深),省电=封面背景+黑色半透明叠加层
|
||||
* (设置 ncm.player_bg_mode: default/power_save)。
|
||||
* - 日夜间切换:主题配色只映射到半透明叠加层(歌词页 scrim)与 accent(红),
|
||||
* 不动沉浸底(恒 NCM_PLAYER_BG)。
|
||||
*
|
||||
@@ -39,6 +40,35 @@
|
||||
#define PLR_EDGE_W 24 /* 左缘返回热区(与 SDK EDGE_W 一致,防抢返回) */
|
||||
#define PLR_SWIPE_MS 65 /* 中部横滑判页阈值(与 SDK SWIPE_THRESH 一致) */
|
||||
|
||||
/* 歌词页布局(§22 重做批):左边距 / 子行高 / 可见行数上限 / 折行段缓冲。
|
||||
* 超宽歌词折行显示(中西文兼容)+ 活动行高亮(纯白)偏上锚定 + 上下行逐级灰阶,
|
||||
* 顶部元信息与歌词正文同左边距。 */
|
||||
#define LYR_PAD 24 /* 歌词页左边距(顶部元信息与歌词正文一致) */
|
||||
#define LYR_SUB_LH 44 /* 一个歌词条目(含折行)的子行高 */
|
||||
#define LYR_NAME_SZ 28 /* 顶部歌名字号 */
|
||||
#define LYR_ART_SZ 20 /* 顶部歌手字号 */
|
||||
#define LYR_CUR_SZ 28 /* 当前歌词行字号 */
|
||||
#define LYR_OTH_SZ 24 /* 上下行歌词字号 */
|
||||
#define LYR_TL_SZ 20 /* 当前行翻译字号 */
|
||||
#define LYR_MAX_ROWS 9 /* 歌词页可见视觉行数上限(含折行,≤9) */
|
||||
#define LYR_SEG_SZ 256 /* 折行段缓冲 */
|
||||
/* 需求3 活动行锚定:块中心相对屏中线偏上 LYR_ABOVE_OFF px;上方至少 LYR_ABOVE_MIN 行已过歌词。 */
|
||||
#define LYR_ABOVE_OFF 50
|
||||
#define LYR_ABOVE_MIN 2
|
||||
/* 需求1 叠加层加深(非纯黑,保留封面微透出 → 歌词清晰、封面隐约):
|
||||
* default=近黑中性,alpha 132→176、色值略暗;power_save=纯黑 alpha 195→200。 */
|
||||
#define LYR_SCRIM_D 0x10101A /* 默认层暗色 scrim */
|
||||
#define LYR_SCRIM_L 0x201A16 /* 默认层亮色 scrim(暖黑) */
|
||||
#define LYR_SCRIM_PS 0x000000 /* 省电层 scrim(纯黑) */
|
||||
#define LYR_SCRIM_A 176
|
||||
#define LYR_SCRIM_PS_A 200
|
||||
|
||||
/* 需求2 歌词行灰阶:按距活动行行距 0..5 分级(0=活动行纯白,越远越灰;最远可见 0x555555)。
|
||||
* 锚点对照用户需求:±1 行 0xBBBBBB、中深 0x808080、最远 0x555555,逐级单调变灰。 */
|
||||
static const uint32_t lyr_gray_tbl[6] = {
|
||||
0xFFFFFF, 0xBBBBBB, 0x929292, 0x808080, 0x6A6A6A, 0x555555
|
||||
};
|
||||
|
||||
typedef struct { meow_view *view; int active; } player_ctrl;
|
||||
static player_ctrl g_plc;
|
||||
|
||||
@@ -63,8 +93,14 @@ static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
|
||||
|
||||
/* ---- 歌词页几何 ---- */
|
||||
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
|
||||
static int g_lyr_lh; /* 行高 */
|
||||
static int g_lyr_meta_y; /* 顶部元信息 y */
|
||||
static int g_lyr_anchor; /* 活动行块中心 y(屏中线偏上,需求3) */
|
||||
static int g_lyr_meta_y; /* 顶部元信息 y(避开状态栏) */
|
||||
/* 歌词布局暂存(每次 plr_draw_lyrics 重建;UI 线程串行,静态复用) */
|
||||
static char s_lyr_seg[LYR_MAX_ROWS][LYR_SEG_SZ]; /* 折行段复用缓冲(一次画一个条目) */
|
||||
static int s_lyr_ent[LYR_MAX_ROWS]; /* 可见条目 → 歌词行下标 */
|
||||
static int s_lyr_rw[LYR_MAX_ROWS]; /* 条目折行段数 */
|
||||
static int s_lyr_sz[LYR_MAX_ROWS]; /* 条目标题字号 */
|
||||
static int s_lyr_y[LYR_MAX_ROWS]; /* 条目块顶 y */
|
||||
|
||||
#define CTL_RR (g_r_side + 8) /* 侧钮命中容差(圆外一点仍可点) */
|
||||
|
||||
@@ -90,6 +126,108 @@ static void fmt_ms(int64_t ms, char *out, size_t sz)
|
||||
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
|
||||
}
|
||||
|
||||
/* 折行:把 txt 按「词/字符」折成 ≤max_rows 段,每段 ≤maxw px。中文按字符、英文按词断行;
|
||||
* 宽测量用 meow_ui_text_width(宿主/生产同口径)。段写入 seg[][LYR_SEG_SZ],返回段数(≥1)。 */
|
||||
static int lyr_wrap_lines(const char *txt, int size, int maxw,
|
||||
char seg[][LYR_SEG_SZ], int max_rows)
|
||||
{
|
||||
if (!txt || !txt[0] || max_rows <= 0) { seg[0][0] = 0; return 1; }
|
||||
int n = 0;
|
||||
size_t len = strlen(txt), pos = 0;
|
||||
while (pos < len && n < max_rows) {
|
||||
size_t start = pos;
|
||||
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
|
||||
int overflow = 0;
|
||||
while (pos < len) {
|
||||
unsigned char c = (unsigned char)txt[pos];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
if (pos + (size_t)ch > len) ch = (int)(len - pos);
|
||||
size_t cand = (pos + (size_t)ch) - start;
|
||||
size_t cp = cand < LYR_SEG_SZ - 1 ? cand : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
if (meow_ui_text_width(seg[n], size) > maxw && cand > 0) { overflow = 1; break; }
|
||||
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
|
||||
pos += (size_t)ch;
|
||||
}
|
||||
if (overflow) {
|
||||
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
|
||||
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
|
||||
unsigned char c = (unsigned char)txt[start];
|
||||
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
|
||||
linelen = (size_t)ch;
|
||||
}
|
||||
size_t cp = linelen < LYR_SEG_SZ - 1 ? linelen : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
pos = start + linelen;
|
||||
if (last_ws > 0) {
|
||||
size_t sp = start + (size_t)last_ws + 1;
|
||||
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
|
||||
}
|
||||
n++;
|
||||
} else {
|
||||
size_t cp = (len - start) < LYR_SEG_SZ - 1 ? (len - start) : LYR_SEG_SZ - 1;
|
||||
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
|
||||
n++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n == 0) { seg[0][0] = 0; n = 1; }
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 歌词可见条目布局:活动行块中心锚定在 g_lyr_anchor(屏中线偏上,需求3 中间偏上),
|
||||
* 活动行上方保底 ≥LYR_ABOVE_MIN 行已过歌词,下方填满,总视觉行数 ≤ LYR_MAX_ROWS(≤9)。
|
||||
* 结果写 s_lyr_ent/rw/sz/y(s_lyr_y 自上向下排好;当前行块中心 = g_lyr_anchor)。 */
|
||||
static int lyr_layout(int n, int cur, int maxw)
|
||||
{
|
||||
/* 清静态暂存(防帧间残留:上帧条目数 > 本周,绘制循环读到陈旧 s_lyr_* 越界复用) */
|
||||
for (int k = 0; k < LYR_MAX_ROWS; k++) { s_lyr_rw[k] = 0; s_lyr_ent[k] = -1; s_lyr_y[k] = 0; }
|
||||
if (cur < 0 || cur >= n) return 0;
|
||||
|
||||
int rc = lyr_wrap_lines(lyric_line_text(cur), LYR_CUR_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (rc > LYR_MAX_ROWS) rc = LYR_MAX_ROWS;
|
||||
|
||||
int cap = LYR_MAX_ROWS - rc; /* 当前行之外可用的视觉行数 */
|
||||
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
|
||||
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
|
||||
if (above_want < LYR_ABOVE_MIN) above_want = (cur >= LYR_ABOVE_MIN) ? LYR_ABOVE_MIN : cur;
|
||||
if (above_want > cap) above_want = cap; /* 总行数 ≤ LYR_MAX_ROWS */
|
||||
|
||||
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
|
||||
int a_cnt = 0, usedA = 0;
|
||||
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (usedA + rr > above_want) break;
|
||||
for (int k = a_cnt; k > 0; k--) {
|
||||
s_lyr_ent[k] = s_lyr_ent[k - 1]; s_lyr_rw[k] = s_lyr_rw[k - 1];
|
||||
s_lyr_sz[k] = s_lyr_sz[k - 1];
|
||||
}
|
||||
s_lyr_ent[0] = i; s_lyr_rw[0] = rr; s_lyr_sz[0] = LYR_OTH_SZ; a_cnt++; usedA += rr;
|
||||
}
|
||||
/* 2) 当前行(下标 a_cnt) */
|
||||
s_lyr_ent[a_cnt] = cur; s_lyr_rw[a_cnt] = rc; s_lyr_sz[a_cnt] = LYR_CUR_SZ;
|
||||
/* 3) 下方(cur+1 向下):余量 = cap - actual 上方行数 */
|
||||
int below_want = cap - usedA;
|
||||
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur; /* 下方行数上限 */
|
||||
int b_cnt = 0, usedB = 0;
|
||||
for (int i = cur + 1; i < n && usedB < below_want; i++) {
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), LYR_OTH_SZ, maxw, s_lyr_seg, LYR_MAX_ROWS);
|
||||
if (usedB + rr > below_want) break;
|
||||
int k = a_cnt + 1 + b_cnt;
|
||||
s_lyr_ent[k] = i; s_lyr_rw[k] = rr; s_lyr_sz[k] = LYR_OTH_SZ; b_cnt++; usedB += rr;
|
||||
}
|
||||
int cnt = a_cnt + 1 + b_cnt;
|
||||
if (cnt == 0) return 0;
|
||||
|
||||
/* 4) y 位置:活动行块中心 = g_lyr_anchor;上方向上加、下方向下加(块间无缝隙) */
|
||||
s_lyr_y[a_cnt] = g_lyr_anchor - rc * LYR_SUB_LH / 2;
|
||||
for (int k = a_cnt + 1; k < cnt; k++)
|
||||
s_lyr_y[k] = s_lyr_y[k - 1] + s_lyr_rw[k - 1] * LYR_SUB_LH;
|
||||
for (int k = a_cnt - 1; k >= 0; k--)
|
||||
s_lyr_y[k] = s_lyr_y[k + 1] - s_lyr_rw[k] * LYR_SUB_LH;
|
||||
return cnt;
|
||||
}
|
||||
|
||||
/* 颜色插值(t=0..255 混向 b)。播放页歌词渐隐/叠加用(无 GPU,逐通道整数混合)。 */
|
||||
static uint32_t mix_c(uint32_t a, uint32_t b, int t)
|
||||
{
|
||||
@@ -177,19 +315,22 @@ static void player_compute_geom(void)
|
||||
g_title_y = g_cover_r.y + g_cover_r.h + 32; /* 400 */
|
||||
g_artist_y = g_title_y + 32; /* 432 */
|
||||
g_qual_y = g_artist_y + 26; /* 458 */
|
||||
g_times_y = g_qual_y + 26; /* 484 */
|
||||
g_bar = NR(48, g_times_y + 20, W - 96, 4); /* 504 (h=4) */
|
||||
g_ctl_y = g_bar.y + 64; /* 568 */
|
||||
/* §22 时间行:放大字号并下移到进度条下方,不与进度条相交。
|
||||
* 布局:进度条(484,h=4) → 时间行(500, font 24) → 控制钮(564)。 */
|
||||
g_bar = NR(48, g_qual_y + 26, W - 96, 4); /* 484 (h=4) */
|
||||
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
|
||||
g_ctl_y = g_times_y + 64; /* 564 */
|
||||
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
|
||||
g_r_side = 24; g_r_play = 34;
|
||||
g_dots_y = g_ctl_y + 62; /* 630 */
|
||||
g_dots_y = g_ctl_y + 62; /* 626 */
|
||||
|
||||
/* 动态带 = 封面下方(元信息/进度/控制/指示点),与封面静态带不重叠。 */
|
||||
g_dyn_r = NR(0, g_cover_r.y + g_cover_r.h, W, H - (g_cover_r.y + g_cover_r.h));
|
||||
|
||||
g_lyr_cx = H / 2; /* 360 */
|
||||
g_lyr_lh = 30;
|
||||
g_lyr_meta_y = 22;
|
||||
/* 歌词页几何:中心线 / 活动行块中心(屏中线偏上,需求3)/ 顶部元信息 y。 */
|
||||
g_lyr_cx = H / 2; /* 360 */
|
||||
g_lyr_anchor = g_lyr_cx - LYR_ABOVE_OFF; /* 310 活动行偏上锚定 */
|
||||
g_lyr_meta_y = ncm_statusbar() + 20;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
@@ -280,8 +421,9 @@ static void plr_draw_cover_dyn(void *ctx)
|
||||
}
|
||||
meow_ui_text_center(W / 2, g_times_y - 8, bbuf, 20, PLR_TEXT_DIM);
|
||||
} else {
|
||||
meow_ui_text(48, g_times_y, lp, 20, PLR_TEXT_DIM);
|
||||
meow_ui_text_right(W - 48, g_times_y, rp, 20, PLR_TEXT_DIM);
|
||||
/* §22 时间字号放大(20→24)且下移到进度条下方,不再与进度条相交 */
|
||||
meow_ui_text(48, g_times_y, lp, 24, PLR_TEXT_DIM);
|
||||
meow_ui_text_right(W - 48, g_times_y, rp, 24, PLR_TEXT_DIM);
|
||||
}
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, PLR_TRACK_BG);
|
||||
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
|
||||
@@ -297,73 +439,75 @@ static void plr_draw_cover_dyn(void *ctx)
|
||||
draw_page_dots(g_dots_y);
|
||||
}
|
||||
|
||||
/* 歌词页(整屏带状物):封面模糊置底 + scrim + 顶部元信息 + 滚动歌词(当前行高亮居中 +
|
||||
* 上下行渐隐)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
|
||||
/* 歌词页(整屏带状物):封面模糊置底 + 加深 scrim + 顶部元信息 + 滚动歌词(活动行纯白偏上锚定 +
|
||||
* 上下行逐级灰阶)+ 指示点。整屏在 进入/切歌/封面变化/换背景模式/换行 时重画;pos tick 不画。 */
|
||||
static void plr_draw_lyrics(void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (meow_ui_activity_active() != ACT_PLAYER || g_ppage != 1) return;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||||
int W = g_W, H = g_H;
|
||||
|
||||
/* 底:封面等比居中放大(contain:等比缩放到屏内最大、居中;四周主题底),无封面 → 沉浸底。
|
||||
* 不拉伸铺满(方图硬拉 480×720 变形丑,2026-09-02 用户报)。
|
||||
* ⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏 → 下次 paint 自锁死,§18)。 */
|
||||
/* 底:封面铺满全屏(cover-fit,四周无黑边;无 GPU 高斯模糊,用低透明中性叠层近似「模糊底」),
|
||||
* 无封面 → 主题中性底(非纯黑)。⚠️ cover_get() 持锁返回,勿在绘前 return(防 g_cm 泄漏,
|
||||
* 下次 paint 自锁死,§18)。§22:default=封面+半透明中性叠层;power_save=封面+黑色更重叠层。 */
|
||||
meow_img *cov = cover_get();
|
||||
meow_ui_fill_rect(0, 0, W, H, NCM_PLAYER_BG);
|
||||
meow_ui_fill_rect(0, 0, W, H, 0x1A1A20); /* 无封面兜底主题底(中性深灰,非纯黑) */
|
||||
if (cov) {
|
||||
int cw = meow_img_w(cov), ch = meow_img_h(cov);
|
||||
if (cw > 0 && ch > 0) {
|
||||
int dw = W, dh = (int)((long)ch * W / cw);
|
||||
if (dh > H) { dh = H; dw = (int)((long)cw * H / ch); }
|
||||
meow_ui_img_scaled((W - dw) / 2, (H - dh) / 2, dw, dh, cov);
|
||||
long dw = W, dh = (long)ch * W / cw; /* cover-fit:铺满宽度,高按等比超出裁剪 */
|
||||
if (dh < H) { dh = H; dw = (long)cw * H / ch; }
|
||||
meow_ui_img_scaled((W - (int)dw) / 2, (H - (int)dh) / 2, (int)dw, (int)dh, cov);
|
||||
}
|
||||
}
|
||||
cover_put();
|
||||
/* 半透明叠加层:default=主题 scrim(日/夜跟随主题);power_save=黑色更重(§22)。 */
|
||||
/* 半透明叠加层:default=主题中性(近黑但不纯黑,封面色彩微透出 → 模糊底观感);
|
||||
* power_save=黑色更重(§22 保留原「封面背景+黑叠加」语义)。需求1 加深:alpha/色值加暗,
|
||||
* 但仍保留「封面隐约」而非纯黑整体吞掉。 */
|
||||
if (g_st.player_bg_mode == NCM_BG_POWER_SAVE)
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 195);
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, LYR_SCRIM_PS, LYR_SCRIM_PS_A);
|
||||
else
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, th->scrim, 150);
|
||||
meow_ui_fill_rect_alpha(0, 0, W, H, g_dark ? LYR_SCRIM_D : LYR_SCRIM_L, LYR_SCRIM_A);
|
||||
|
||||
/* 顶部元信息(title · artist,Apple Music 式顶部小字) */
|
||||
/* 顶部元信息(两行居左):第一行歌名(大字)、第二行歌手名(小字,多歌手保留已拼串)。
|
||||
* 与歌词正文同左边距 LYR_PAD;下移避开状态栏(g_lyr_meta_y = statusbar + 20)。 */
|
||||
ncm_song_t sm;
|
||||
if (cur_song_meta(&sm) == 0) {
|
||||
char mb[300];
|
||||
snprintf(mb, sizeof mb, "%s / %s", sm.name, sm.art_name);
|
||||
meow_ui_text_center(W / 2, g_lyr_meta_y, ncm_text_fit(mb, W - 48, 18), 18, PLR_TEXT);
|
||||
meow_ui_text(LYR_PAD, g_lyr_meta_y,
|
||||
ncm_text_fit(sm.name, W - 2 * LYR_PAD, LYR_NAME_SZ), LYR_NAME_SZ, PLR_TEXT);
|
||||
meow_ui_text(LYR_PAD, g_lyr_meta_y + LYR_NAME_SZ + 6,
|
||||
ncm_text_fit(sm.art_name, W - 2 * LYR_PAD, LYR_ART_SZ), LYR_ART_SZ, PLR_TEXT_DIM);
|
||||
}
|
||||
|
||||
/* 歌词:当前行居中高亮;上下行渐隐(距中心越远越接近底色)。 */
|
||||
/* 歌词:活动行纯白居中(含折行,块中心偏上锚定);上下行按行距逐级灰阶;≤LYR_MAX_ROWS。 */
|
||||
int n = lyric_count();
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur = (n > 0) ? lyric_line_at(ps) : -1;
|
||||
int cy = g_lyr_cx;
|
||||
int lo = g_lyr_meta_y + 44, hi = g_dots_y - 6; /* 歌词窗口(避顶部元信息/底部指示点) */
|
||||
for (int i = cur - 7; i <= cur + 6; i++) {
|
||||
if (i < 0 || i >= n) continue;
|
||||
int yy = cy + (i - cur) * g_lyr_lh;
|
||||
if (yy < lo || yy + g_lyr_lh > hi) continue;
|
||||
int maxw = W - 2 * LYR_PAD;
|
||||
int cnt = lyr_layout(n, cur, maxw);
|
||||
/* 渐隐目标色 = 歌词页背底层(与 default/power_save 叠层一致),供翻译行融入背景 */
|
||||
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? LYR_SCRIM_PS
|
||||
: (g_dark ? LYR_SCRIM_D : LYR_SCRIM_L);
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int i = s_lyr_ent[k], rows = s_lyr_rw[k], sz = s_lyr_sz[k], y = s_lyr_y[k];
|
||||
int is_cur = (i == cur);
|
||||
int dist = (i - cur) < 0 ? (cur - i) : (i - cur);
|
||||
int fade = dist * 42; /* 0..255,越远越淡 */
|
||||
if (fade > 214) fade = 214; /* 远行仍隐约可见,不全隐 */
|
||||
uint32_t base = is_cur ? pa->red_lit : PLR_TEXT;
|
||||
uint32_t bgcol = (g_st.player_bg_mode == NCM_BG_POWER_SAVE) ? 0x000000 : th->scrim;
|
||||
uint32_t col = mix_c(base, bgcol, fade);
|
||||
int size = is_cur ? 24 : 20;
|
||||
meow_ui_text_center(W / 2, yy, ncm_text_fit(lyric_line_text(i), W - 24, size), size, col);
|
||||
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur)); /* 距活动行行距 */
|
||||
uint32_t col = lyr_gray_tbl[dist > 5 ? 5 : dist]; /* 0=纯白,越远越灰 */
|
||||
int rr = lyr_wrap_lines(lyric_line_text(i), sz, maxw, s_lyr_seg, rows);
|
||||
if (rr > rows) rr = rows;
|
||||
for (int r = 0; r < rr; r++)
|
||||
meow_ui_text(LYR_PAD, y + r * LYR_SUB_LH, s_lyr_seg[r], sz, col);
|
||||
if (is_cur) {
|
||||
const char *tl = lyric_line_tl(i);
|
||||
if (tl && tl[0])
|
||||
meow_ui_text_center(W / 2, yy + 28, ncm_text_fit(tl, W - 24, 18), 18,
|
||||
mix_c(pa->red_lit, bgcol, 120));
|
||||
meow_ui_text(LYR_PAD, y + rows * LYR_SUB_LH + 2,
|
||||
ncm_text_fit(tl, W - 2 * LYR_PAD, LYR_TL_SZ), LYR_TL_SZ,
|
||||
mix_c(0xFFFFFF, bgcol, 120));
|
||||
}
|
||||
}
|
||||
if (n == 0) {
|
||||
meow_ui_text_center(W / 2, cy - 10,
|
||||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
|
||||
meow_ui_text(LYR_PAD, g_lyr_cx - 10,
|
||||
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…", 20, PLR_TEXT_DIM);
|
||||
}
|
||||
|
||||
draw_page_dots(g_dots_y);
|
||||
@@ -466,21 +610,27 @@ static void player_reset_touch(void)
|
||||
g_touch_on = 0; g_drag_latched = 0; g_drag_dx = 0;
|
||||
}
|
||||
|
||||
/* 歌词页:点行 seek(把 tap y 归到最近一行的下标)。 */
|
||||
/* 歌词页:点行 seek(用与绘制一致的可见条目布局把 tap y 命中最临近行;
|
||||
* 活动行已偏上锚定,非屏中线,故按 s_lyr_y/rw 逐块扫描而非 cx 偏移)。 */
|
||||
static int lyrics_line_tap(int y)
|
||||
{
|
||||
int n = lyric_count();
|
||||
if (n <= 0) return -1;
|
||||
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。
|
||||
* 元信息块 = 歌名(28)+6+歌手(20)+边距 → g_lyr_meta_y+66。 */
|
||||
if (y < g_lyr_meta_y + 66 || y > g_dots_y - 6) return -1;
|
||||
int64_t ps = player_manager_pos_ms();
|
||||
int cur = lyric_line_at(ps);
|
||||
int off = y - g_lyr_cx;
|
||||
int dioff = (off >= 0) ? (off + g_lyr_lh / 2) / g_lyr_lh : (off - g_lyr_lh / 2) / g_lyr_lh;
|
||||
int i = cur + dioff;
|
||||
if (i < 0) i = 0;
|
||||
if (i >= n) i = n - 1;
|
||||
/* 仅当 tap 落在歌词窗口内才响应(顶部元信息/底部指示点区不误触)。 */
|
||||
if (y < g_lyr_meta_y + 44 || y > g_dots_y - 6) return -1;
|
||||
return i;
|
||||
int maxw = g_W - 2 * LYR_PAD;
|
||||
int cnt = lyr_layout(n, cur, maxw);
|
||||
if (cnt <= 0) return -1;
|
||||
for (int k = 0; k < cnt; k++) {
|
||||
int yb = s_lyr_y[k];
|
||||
int yh = s_lyr_rw[k] * LYR_SUB_LH;
|
||||
if (y >= yb && y < yb + yh) return s_lyr_ent[k];
|
||||
}
|
||||
/* 落在可见块之外 → 就近取整(顶部元信息/底部指示点已在上面排除) */
|
||||
return (y < s_lyr_y[0]) ? s_lyr_ent[0] : s_lyr_ent[cnt - 1];
|
||||
}
|
||||
|
||||
static int in_circle(int cx, int cy, int r, int x, int y)
|
||||
|
||||
@@ -268,6 +268,17 @@ static void *playlist_worker(void *arg)
|
||||
/* 首屏:快照式加载(detail 全量 trackIds + 物化首段)。ctx 校验代次/目标,过期自动丢弃。 */
|
||||
if (tidx < 0) rc = NCM_ERR_ARG;
|
||||
else rc = appstate_load_playlist_songs_ctx(&tpl, tsearch, tgen, tidx, 0, NCM_SONG_BATCH);
|
||||
/* §22 整张歌单逐渐物化:首屏成功后把「能装进滑动窗口」的歌单一次性补填入窗
|
||||
* (≤NCM_SONG_WINDOW 的歌单整张物化,末叶行不再恒「.../加载中」——真机反馈
|
||||
* 「曲目稍多时行一直加载中,点开播放这首才回填」);超窗口的歌单仍按视口滑动补载
|
||||
* (内存纪律:窗口 ≤256 首)。窗口基址=0 覆盖 [0,min(track,WINDOW)),不滑窗不挤占视口。 */
|
||||
if (rc == NCM_OK) {
|
||||
int full = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
|
||||
if (full > NCM_SONG_BATCH) {
|
||||
int r2 = appstate_song_ensure_range(0, full);
|
||||
if (r2 != NCM_OK && rc == NCM_OK) rc = r2; /* 补填失败非致命:已物化槽保留 */
|
||||
}
|
||||
}
|
||||
/* REPORT §30(真机四连症状①):首屏结果打日志——tidx/track_ids_count/物化数/rc,
|
||||
* 供「162 首歌单所有条目占位 加载中」定位(首屏 song_detail 失败/部分失败=非致命
|
||||
* g_ens_err=rc,窗口空 → 行占位;g_st.song_count 为诊断竞态读,仅日志用途)。 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* ui/settings.c — 设置页(ACT_SETTINGS)。
|
||||
*
|
||||
* 行项:音质(§20 八档:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声,
|
||||
* 默认极高 HQ;每档标注规格+会员门槛 CVIP/SVIP;超权限档允许选定,播放时钳顶+逐级降级,
|
||||
* 默认极高 HQ;每档标注规格+会员门槛(畅听会员/黑胶 VIP/超级会员);超权限档允许选定,播放时钳顶+逐级降级,
|
||||
* 见 player_manager.c song_local_file)+ 缓存上限(默认 10GB,上限 100GB,支持预设与
|
||||
* 「自定义」数字输入)+ 清理缓存(显示当前占用)+ 退出登录 + 关于/版本。
|
||||
* 设置持久化到 MeowData/dev.player.ncm/data(meow_config_*,与 core
|
||||
@@ -80,9 +80,12 @@ static void settings_draw(void *ctx)
|
||||
rows[4].title = "退出登录"; rows[4].red = 1;
|
||||
rows[5].title = "关于网易云"; rows[5].red = 0;
|
||||
{
|
||||
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20) */
|
||||
/* 当前档 + 会员门槛标注;超权限(未解锁)时提示将降级(不硬拒,§20)。
|
||||
* 门槛串按用户语义对齐:1=畅听会员 / 2=黑胶 VIP / 3=超级会员(门槛恒 ≤2,SUPER 兜底)。 */
|
||||
int gate = ncm_api_level_gate(g_st.level);
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : "";
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (gate == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (gate == NCM_VIP_SUPER) ? "超级会员" : "";
|
||||
int locked = (account_vip() < gate);
|
||||
if (gate_s[0])
|
||||
snprintf(rows[0].sub, sizeof rows[0].sub, "当前: %s (%s) / 需 %s%s",
|
||||
@@ -112,7 +115,7 @@ static void settings_draw(void *ctx)
|
||||
|
||||
/* 品牌红页脚小字。 */
|
||||
int ny = y + SETTINGS_ROWS * (g_row_h + 8) + 4;
|
||||
meow_ui_text(x, ny, "无损/Hi-Res 需 CVIP ・ 臻音/母带需 SVIP,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny, "无损/Hi-Res 需畅听会员 ・ 臻音/母带需黑胶 VIP,不支持时自动降级", 20, th->onSurfaceVariant);
|
||||
meow_ui_text(x, ny + 24, "网易云音乐 / MeowOS 移植版", 20, pa->red);
|
||||
}
|
||||
|
||||
@@ -170,9 +173,11 @@ int settings_on_tap(int x, int y)
|
||||
int lv = g_ui_levels[i];
|
||||
int gate = ncm_api_level_gate(lv);
|
||||
char lbl[96];
|
||||
/* 每档标注 规格+会员门槛;未解锁档标注「需 CVIP/SVIP」(sheet 无置灰能力,
|
||||
/* 每档标注 规格+会员门槛;未解锁档标注「需 畅听会员/黑胶 VIP」(sheet 无置灰能力,
|
||||
* 以文字标注代替——允许选定,实际播放逐级降级到可用档) */
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "SVIP" : (gate == NCM_VIP_CVIP) ? "CVIP" : NULL;
|
||||
const char *gate_s = (gate == NCM_VIP_SVIP) ? "黑胶 VIP"
|
||||
: (gate == NCM_VIP_CVIP) ? "畅听会员"
|
||||
: (gate == NCM_VIP_SUPER) ? "超级会员" : NULL;
|
||||
if (gate_s)
|
||||
snprintf(lbl, sizeof lbl, "%s (%s) / %s%s", ncm_level_name(lv),
|
||||
ncm_level_spec(lv), gate_s, (vip < gate) ? "/未解锁" : "");
|
||||
|
||||
Reference in new issue
Block a user