Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ncm_app_internal.h
T
AskaEth 3d70a3dd2c 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 推卡自刷验收全绿
2026-09-03 12:16:01 +08:00

546 lines
36 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ncm_app_internal.h — dev.player.ncm 应用内部共享头(UI 各页 + app 装配层 + 宿主测试面)。
*
* 层级(对齐 hello_app/app.c 批② + files_app):
* - app.c main + 装配 + 事件分发(把 meow_ui_run 的平铺回调路由到各页)
* - app/ui/<page>.c 每页一个 view 文件(login/home/playlist/player/settings)
* - app/ui/common/chrome.c 共享 chrome(topbar / 内容区几何 / 空态 / 提示)
* - app/logic/ appstate(全局 ctx/歌单/歌曲/播放态)+ lyric(LRC 解析) + cover(封面缓存)
* - app/play/ player_manager(播放线程) + audio_out(audiod/ALSA/文件输出) + ipc_media(媒体会话 IPC)
*
* 铁律(APPS.md §3.4.1 / WORK_STATE.md):物理键禁映射——on_key 不处理任何键语义;
* 返回=左缘左滑(on_swipe LEFT)/返回钮/meow_ui_request_exit;左缘返回:播放页→歌单→主页→退出。
* 播放/网络/IPC 用独立线程,UI 不卡。媒体会话经 meow_ipc.h 帧格式最小客户端上报
* MSG_META + 收 MSG_PLAYCTL(SDK 尚无媒体封装,App 内实现)。
*/
#ifndef NCM_APP_INTERNAL_H
#define NCM_APP_INTERNAL_H
#include <stdint.h>
#include <stddef.h>
#include <pthread.h>
#include <limits.h>
#include "meow_ui.h"
#include "meow_ui_view.h"
#include "ncm.h" /* 核心库伞头(ctx/api/login/cache/...) */
/* ---- Activity 归属(§6.1 ①)---- */
#define ACT_LOGIN 0 /* 扫码登录页(根,未登录) */
#define ACT_HOME 1 /* 我的歌单(根/主页,已登录) */
#define ACT_PLAYLIST 2 /* 歌单详情(曲目列表) */
#define ACT_PLAYER 3 /* 播放页(可横竖屏切,v1 允许切) */
#define ACT_SETTINGS 4 /* 设置页 */
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
* 缩略 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 "640y640"
/* ---- 布局 ---- */
#define APP_W 480
#define APP_H 720
#define APP_TOPBAR_H 56
#define APP_ROW_H 56 /* 列表行高 */
#define APP_PAD 16
#define APP_OP_W 48 /* 行右侧操作区触达宽 */
#define APP_FOOTER_H 26
/* ---- 播放页(§22 重做:封面页/歌词页双页,NCM_CACHE_DESIGN §6)----
* 沉浸底恒 NCM_PLAYER_BG(不随系统明暗反转——「不动沉浸底」);日夜间主题配色只映射到
* 半透明叠加层(歌词页 scrim)与 accent(红)。 */
#define NCM_PLAYER_BG 0x101014 /* 播放页固定沉浸底(中性近黑) */
/* 歌词页背景模式(设置 ncm.player_bg_mode:default/power_save,§22) */
#define NCM_BG_DEFAULT 0 /* default:封面模糊置底(封面缩放铺底 + 主题 scrim 叠加) */
#define NCM_BG_POWER_SAVE 1 /* power_save:封面背景 + 黑色半透明叠加层(更暗,省电观感) */
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
/* 滑动窗口(REPORT §17,三期第三批,取代 §14 的 NCM_SONG_MAX=1000 硬截断):
* song_ids[NCM_TRACKID_MAX] = detail(/v6/playlist/detail, n=1)取回的全量 trackIds
* (int64×16384=128KB bss,支撑任意长度歌单,不再 1000 截断);
* songs[NCM_SONG_WINDOW] = 物化滑动窗口(256 首 × ~608B ≈ 152KB bss,与歌单长度无关);
* song_valid[NCM_SONG_WINDOW]= 槽位有效位图(songs[i] ↔ 全局下标 window_base+i);
* 滚动出窗的槽位失效(数据已序列化到 <cache_dir>/meta/<id>.bin),入窗经 song_detail 按需
* 拉取(盘缓存命中免请求)。列表 count = track_ids_count(全量),占位行驱动后台 ensure。 */
#define NCM_TRACKID_MAX 16384 /* 全量 trackIds 索引上限(128KB bss) */
#define NCM_SONG_WINDOW 256 /* 滑动窗口大小(内存物化歌曲对象数上限) */
#define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
* 窗口补载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB) */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 8*1024*1024 /* 封面下载字节上限(REPORT §30:网易云封面全尺寸 ~1~2MB,
* 旧 288KB 静态缓冲恒溢出 → ncm_http NCM_ERR_FULL → 封面下载失败×8放弃。
* §30 改为流式写文件(ncm_http_download 回调写 cache 临时文件),
* 本值是下载字节上限(非缓冲),不再占用 bss——设备 RAM 仅 56MB。) */
/* ---- 搜索(REPORT §23:独立结果数组 + 单曲/歌单两类页签 + 历史 + 分页)---- */
#define NCM_SEARCH_PAGE 30 /* cloudsearch limit/页(用户定案;offset 累加) */
#define NCM_SEARCH_SONG_MAX 256 /* 单曲结果累计上限(独立数组 ≈156KB bss,不覆写歌单窗口) */
#define NCM_SEARCH_PL_MAX 60 /* 歌单结果累计上限(≈48KB bss) */
#define NCM_SEARCH_HIST_MAX 12 /* 搜索历史条数(上次在最新,≤12) */
#define NCM_SEARCH_HIST_LEN 128 /* 单条历史长度 */
/* 播放队列来源(§23:搜索单曲起播建立「搜索队列」上下文,不覆写歌单窗口) */
#define NCM_QUEUE_PLAYLIST 0 /* 默认:歌单滑动窗口 */
#define NCM_QUEUE_SEARCH 1 /* 搜索单曲结果队列 */
/* active_playlist 哨兵:搜索歌单详情(复用 playlist_open 链路,读 g_st.search_pl_open) */
#define NCM_PL_SEARCH (-2)
/* 线程创建:显式小栈(内存瘦身,OOM 修复)。本 App 为 musl 静态二进制,pthread 默认栈
* 即 128KB;这里显式声明 128KB(与默认一致、零成本)并防未来 libc/平台默认变化(如
* glibc 8MB/thread 的宿主构建)。实测网络/解码调用链栈深 ≤~30KB,128KB 充足。
* 返回 pthread_create 结果。 */
#define NCM_THREAD_STACK (128 * 1024)
static inline int ncm_thread_create(pthread_t *t, void *(*fn)(void *), void *arg)
{
pthread_attr_t a;
pthread_attr_init(&a);
pthread_attr_setstacksize(&a, (size_t)NCM_THREAD_STACK);
int r = pthread_create(t, &a, fn, arg);
pthread_attr_destroy(&a);
return r;
}
/* ---- 播放/媒体会话状态(play 线程 ↔ UI/IPC 共享,mu 保护)---- */
enum {
NCM_PC_STOPPED = 0,
NCM_PC_PLAYING = 1,
NCM_PC_PAUSED = 2,
NCM_PC_BUFFER = 3, /* 缓冲/加载中 */
};
/* 播放器命令(IPC MSG_PLAYCTL cmd 一一对应)*/
enum {
NCM_CTL_PLAY = 1,
NCM_CTL_PAUSE = 2,
NCM_CTL_PREV = 3,
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 线程读)。 */
typedef struct ncm_play_run {
pthread_mutex_t mu;
int state; /* NCM_PC_* */
int cur_song; /* 当前曲目在 g_st.songs[] 下标;-1=无 */
int64_t pos_ms; /* 当前播放位置(播放进程推进,seek 写此) */
int64_t dur_ms; /* 当前曲目标称时长 */
int cmd; /* 暂存最新内部命令(play/pause/prev/next/seek)*/
int64_t cmd_seek_ms; /* cmd==SEEK 目标 */
int seq; /* 播放代际(每次 start 递增,用于区分过期信号)*/
int err; /* 最近一次播放错误(0=无) */
char errmsg[96];
int64_t dl_done; /* 本曲已下载字节(缓冲期进度反馈,§18) */
int64_t dl_total; /* 本曲标称大小(song_url 返回;0=未知) */
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
int eff_level; /* 实际生效音质档(§20;≠用户档时播放页标注「已降级至 xx」;-1=未定) */
/* 边下边播统计(§21,stream_src 搬运,播放线程写/UI 读) */
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;
int play_song_valid; /* 1=快照有效(stop 清 0) */
} ncm_play_run_t;
/* ---- 音质档位字符串(设置页 + 持久化用)---- */
extern const char *media; /* 占位 */
/* ---- App 全局状态(main 线程拥有;play 线程经 ncm_play_run 共享)---- */
typedef struct ncm_app_state {
ncm_ctx_t ctx; /* 核心库上下文(device_id/cookie/cache) */
char data_dir[512]; /* 数据目录(MeowData/dev.player.ncm/data) */
char cache_dir[512];
/* 登录 */
char unikey[64];
char qr_url[256];
unsigned char qr_mods[177*177]; /* QR 位图矩阵(1=暗) */
int qr_size;
int login_state; /* NCM_LOGIN_* */
int logged_in;
/* 账号 */
char uid[32];
char nickname[128];
/* 会员态(§20,account.json 落盘:data/account.json;刷新=登录后/超 1 天/到期后一次) */
int vip; /* NCM_VIP_*(NONE/CVIP/SVIP) */
int64_t vip_expire_ms; /* 当前档到期时间(ms;0=无/未知) */
int64_t vip_cvip_exp_ms; /* 音乐包/畅听到期(ms;account.json cvip_expire_ms,落盘保真) */
int64_t vip_svip_exp_ms; /* 黑胶到期(ms;account.json svip_expire_ms) */
int64_t vip_checked_at; /* vip/info 最近获取时间(秒) */
/* MUSIC_U 临期续期(§NEW,用户定案):cookie 最近一次成功续期时间(秒;account.json
* last_refresh_ts;0=缺失/旧会话视为临期)。App 启动时后台静默刷新一次,不打断播/不卡 UI。 */
int64_t cookie_last_refresh_ts;
/* 我的歌单 */
ncm_playlist_t playlists[NCM_PLAYLIST_MAX];
int playlist_count;
int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
* 滑动窗口模型(REPORT §17):
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_TRACKID_MAX,全局下标索引);
* songs[] = 物化滑动窗口,songs[i] ↔ 全局下标 window_base+i;
* song_valid[] = 槽位有效位图(出窗=失效,入窗=物化后置位);
* window_base = 窗口起始全局下标;song_count = 窗口内有效槽位数(≤ NCM_SONG_WINDOW);
* track_ids_count = trackIds 总数(列表 count = track_ids_count,任意长度无截断)。
* 窗口读写由 appstate.c 内 g_song_mu 保护;外部一律走 appstate_song_copy/ensure_range。 */
int64_t song_ids[NCM_TRACKID_MAX];
ncm_song_t songs[NCM_SONG_WINDOW];
uint8_t song_valid[NCM_SONG_WINDOW];
int window_base;
int song_count;
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_TRACKID_MAX) */
/* 播放页视图态 */
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
meow_img *cover_img; /* 当前封面(解码后;UI 持有,play 线程不碰) */
char cover_url[512]; /* 当前封面 URL(playlist/song cover) */
/* 歌词(播放器加载后由 play 线程填充,UI 读)*/
pthread_mutex_t lyric_mu;
int lyric_line_count;
int cur_lyric_line;
/* 设置(持久化 MeowData/dev.player.ncm/data)*/
int level; /* NCM_LEVEL_* */
int cache_limit_gb;
int player_bg_mode; /* 歌词页背景模式(§22):NCM_BG_DEFAULT / NCM_BG_POWER_SAVE */
/* 搜索(REPORT §23:独立结果数组,不覆写歌单窗口;单曲/歌单两类页签各自独立结果集/分页/滚动) */
char search_query[256];
volatile int search_loading; /* 1=搜索请求在途 */
int search_err; /* 最近一次搜索错误(0=ok) */
int search_tab; /* 当前页签:0=单曲 1=歌单 */
ncm_song_t search_songs[NCM_SEARCH_SONG_MAX]; /* 单曲结果(独立数组) */
int search_song_count;
int search_song_offset; /* 下次 offset(=已加载条数) */
int search_song_total; /* 服务端总数(-1=未知) */
int search_song_more; /* 1=单曲还有下一页 */
ncm_playlist_t search_pls[NCM_SEARCH_PL_MAX]; /* 歌单结果(独立数组) */
int search_pl_count;
int search_pl_offset;
int search_pl_total;
int search_pl_more;
/* 搜索歌单打开的详情(active_playlist==NCM_PL_SEARCH 时用;复用 playlist_open 链路) */
ncm_playlist_t search_pl_open;
/* 播放队列来源(§23):默认 NCM_QUEUE_PLAYLIST;搜索单曲起播切 NCM_QUEUE_SEARCH */
int queue_src;
} ncm_app_state_t;
extern ncm_app_state_t g_st; /* 全局状态(app.c 定义) */
extern ncm_play_run_t g_play; /* 播放运行态(app.c 定义) */
extern volatile int g_app_quit; /* 请求退出(线程停) */
extern volatile int g_net_ready; /* 网络/核心库线程安全可用 */
extern int g_host_mock; /* 1=宿主/测试:跳过网络,用预置数据 */
/* ---- App 侧调色板(ui/common/ncm_colors.c)----
* 品牌红 + 播放页/登录专属色。中性色由 meow_theme_get() 语义色提供。§3.2/§3.3/§5.4。 */
typedef struct ncm_pal {
uint32_t red; /* 主 accent:day #C20C0C / night #EC4141 */
uint32_t red_lit; /* 亮点变体:day #EC4141 / night #FF6A6A */
uint32_t red_ctr; /* 红底浅块:day #FBE9E9 / night #3A1919 */
uint32_t on_red; /* 红底前景(#FFFFFF) */
uint32_t on_red_dim; /* 红底次前景(白 ~88%,文本用) */
uint32_t hero; /* 登录 hero 深红压场:day #C20C0C / night #7A0E0E */
uint32_t player_bg; /* 播放页沉浸底:day #FFFDF9 / night #0C0D10 */
uint32_t disc; /* 黑胶盘体:day #1A1A1A / night #141414 */
uint32_t disc_edge; /* 盘缘槽纹:day #2A2A2A / night #232323 */
} ncm_pal_t;
extern int g_dark; /* 1=黑红(night) 0=白红(day) */
const ncm_pal_t *ncm_palette(int dark); /* dark==g_dark */
void ncm_on_theme(meow_theme_mode m); /* 主题侦听:明暗变化 → 整 App 重绘 */
void ncm_fill_round(int x, int y, int w, int h, int r, uint32_t color); /* 填充圆角矩形 */
void ncm_draw_button(int x, int y, int w, int h, const char *label,
uint32_t bg, uint32_t fg); /* App 侧品牌红/FILLED 按钮 */
int first_glyph(const char *s, char *out, size_t sz); /* 取首个 UTF-8 字形(头像首字) */
const char *ncm_text_fit(const char *s, int maxw, int size); /* 按宽裁剪("..."结尾;静态缓冲) */
/* ---- 通用矩形 ---- */
typedef struct { int x, y, w, h; } ncm_rect_t;
#define NR(x,y,w,h) ((ncm_rect_t){ (x),(y),(w),(h) })
/* ---- chrome(ui/common/chrome.c)---- */
int ncm_statusbar(void);
ncm_rect_t ncm_topbar_rect(void);
ncm_rect_t ncm_content_rect(void);
meow_widget *ncm_topbar_create(const char *title, int back, const char *action_label);
void ncm_draw_empty(ncm_rect_t r, const char *kind, const char *what);
void ncm_draw_footer(ncm_rect_t r, const char *text);
void ncm_snack(const char *text); /* 底部 2s 提示条(非阻塞) */
void chrome_tick(void); /* 周期检查 snack 到期清除 */
/* ---- 页面(ui/<page>.c)---- */
/* login */
meow_view *login_view_create(void);
int login_is_active(void);
void login_on_tap(int x, int y);
void login_tick(uint64_t now);
int login_start(void); /* 触发 unikey 获取(宿主测试:无网络则返回) */
const char *login_status_str(int state);
/* home */
meow_view *home_view_create(void);
int home_is_active(void);
int home_on_tap(int x, int y);
void home_swipe(int dir, int dist);
void home_touch_down(int x, int y);
void home_touch_move(int x, int y);
void home_reload(void); /* 刷新我的歌单(后台 worker,非阻塞) */
void home_tick(uint64_t now); /* UI tick:检查歌单 worker 是否完成并刷新 */
int home_go_up(void); /* 左缘返回:到底则退出 */
/* playlist */
meow_view *playlist_view_create(void);
int playlist_is_active(void);
int playlist_on_tap(int x, int y);
void playlist_swipe(int dir, int dist);
void playlist_touch_down(int x, int y);
void playlist_touch_move(int x, int y);
void playlist_open(int idx); /* 打开第 idx 个歌单(下拉进歌单详情) */
void playlist_open_pl(const ncm_playlist_t *pl); /* 打开「搜索来的歌单」详情(§23,复用链路) */
void playlist_refresh(void);
void playlist_tick(uint64_t now);/* UI tick:检查歌单 worker 是否完成并刷新 */
/* player */
meow_view *player_view_create(void);
int player_is_active(void);
void player_on_tap(int x, int y);
void player_tick(uint64_t now); /* 播放进度/歌词滚动 */
void player_play_song(int idx); /* 从歌单窗口第 idx 首开始播放(queue=PLAYLIST) */
void player_play_search_song(int i); /* 从搜索单曲第 i 首开始播放(queue=SEARCH,§23) */
void player_pause_toggle(int on);
void player_next(int dir); /* dir=+1 下一首 / -1 上一首 */
void player_seek(int64_t ms);
void player_on_swipe(int dir, int dist); /* 中部横滑页切换(DIR=RIGHT→封面页;PPAGE 由 touch 拖拽+on_tap 判向) */
int player_set_page(int page); /* 0=封面页 1=歌词页;返回旧页。拖动/程序切页用 */
int player_page(void); /* 当前页(0/1) */
void player_touch_down(int x, int y); /* 中部横滑跟踪(与左缘返回/顶部通知分流,§22) */
void player_touch_move(int x, int y);
int player_set_screen(int landscape); /* 兼容:重做后恒竖屏(返回旧值 0) */
int player_screen(void);
/* settings */
meow_view *settings_view_create(void);
int settings_is_active(void);
int settings_on_tap(int x, int y);
void settings_swipe(int dir, int dist);
void settings_touch_down(int x, int y);
void settings_touch_move(int x, int y);
void settings_open(void); /* 进设置页 */
void settings_reload(void);
/* search */
meow_view *search_view_create(void);
int search_is_active(void);
int search_on_tap(int x, int y);
void search_swipe(int dir, int dist);
void search_touch_down(int x, int y);
void search_touch_move(int x, int y);
void search_open(void); /* 进搜索页 */
void search_submit(const char *kw); /* 用户确认后发起搜索(风控:仅在确认时发) */
void search_tick(uint64_t now); /* UI tick:检查搜索 worker 完成并刷新 */
int search_swipe_left(void); /* 历史模式左滑删单条:1=已消费(不触返回) */
/* ---- logic ---- */
int appstate_init(void); /* 初始化 ctx/data/cookie/设置;0=成功 */
void appstate_load_settings(void);
void appstate_save_settings(void);
void appstate_set_logged_in(const char *uid, const char *nickname);
void appstate_logout(void);
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
int appstate_load_playlists(void); /* 拉我的歌单 */
int appstate_load_playlist_songs(int idx); /* 兼容:载索引 + 首个窗口(内部走 ensure) */
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
/* 兼容入口(滑动窗口语义,REPORT §17):
* offset=0 重头(detail 全量 trackIds 重建索引、清窗口、
* 物化 [0,limit));offset>0 = 物化 [offset,offset+limit)
* (滑窗+盘缓存+song_detail 补载,内部 ensure_range)。 */
int appstate_song_ensure_range(int lo, int hi);
/* 滑动窗口核心:确保全局下标 [lo,hi) 已物化。
* 窗口未覆盖先滑窗(重叠区保留,出窗槽位失效);
* 缺失槽位先读盘缓存(<cache_dir>/meta/<id>.bin),
* miss 攒批 ≤NCM_SONG_BATCH 走 song_detail,成功落盘。
* 批内部分返回/单批网络失败不再整段置失败——已物化槽保留,
* 缺槽留 invalid(下次 ensure 再试),返回仍为 NCM_OK(§26)。
* worker 线程调用(含网络);返回 NCM_OK/负错误码。 */
int appstate_range_loaded(int lo, int hi); /* 1=[lo,hi) 全部在窗内且有效(UI 判是否需 ensure) */
int appstate_song_copy(int gidx, ncm_song_t *out); /* 锁内读窗:0=已物化拷出;-1=窗外/未物化 */
int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化(窗→盘→song_detail 单曲),
* 不滑窗(播放线程用,避免与浏览视口抢窗口) */
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
* 同步 song_ids/track_ids_count(播放上下文) */
/* ---- 歌单加载上下文(§26 开A→未载完→开B 竞态防串)----
* UI(playlist.c)在每次打开/刷新歌单时调用 appstate_playlist_begin 记录「代次+目标」,
* worker 用同一代次调 appstate_load_playlist_songs_ctx;加载完成时若代次/目标已不匹配,
* 结果判过期丢弃(NCM_ERR_STALE),防「前一次加载的 worker 结果落进新打开的歌单」。 */
int appstate_playlist_open_gen(void); /* 当前打开代次(0=从未打开) */
int appstate_playlist_open_idx(void); /* 当前打开目标下标(-2=搜索歌单哨兵;-1=无) */
int appstate_playlist_begin(int idx); /* 递增代次并记目标;返回新代次(UI 打开/刷新时调) */
void appstate_playlist_reset(void); /* 清窗口/索引(UI 丢弃过期结果、在播安全)。加锁。 */
/* 窗口归属(§NEW bug B:新进歌单显示上一个歌单内容)——
* worker 首屏发布窗口后 appstate_playlist_window_set 标记
* 归属歌单 id;playlist_refresh 在换歌单前用 window_is 判
* 目标≠归属 → 同步 reset 清窗,防「开 B 显示 A」。 */
void appstate_playlist_window_set(const char *plid); /* 标记当前窗口归属歌单 id */
int appstate_playlist_window_is(const char *plid); /* 1=窗口归属该歌单(换歌单清窗判据) */
int appstate_load_playlist_songs_ctx(const ncm_playlist_t *pl, int is_search,
int ctx_gen, int ctx_idx, int offset, int limit);
/* 快照式加载(歌单详情/曲目物化):
* pl = 打开时快照(不读 g_st.playlists[],防
* home_worker 并发 memset 污染 id);
* is_search = 1=搜索歌单(NCM_PL_SEARCH 哨兵);
* ctx_gen/ctx_idx = begin() 返回的代次与目标,加载中/末
* 验不匹配 → NCM_ERR_STALE(不写全局);
* ctx_gen < 0 = 不校验(兼容既有调用方/测试)。
* offset=0 重头(detail 全量 trackIds 重建索引+清窗口);
* offset>0 = 物化 [offset,offset+limit)(滑窗+盘缓存+补载)。 */
/* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)----
* 快照 <data_dir>/playlists.bin(magic/ver/时间戳 + 数组)。进首页先渲染缓存秒开,
* 后台 worker 拉网络成功后覆写缓存(stale-while-revalidate);断网时列表可离线浏览。 */
int appstate_playlists_cache_load(int64_t *ts_out); /* 0=命中(g_st.playlists/playlist_count 已填) */
int appstate_playlists_cache_save(void); /* 把当前 g_st.playlists/playlist_count 落盘 */
/* ---- 播放队列来源(§23)---- */
void appstate_set_queue(int src); /* NCM_QUEUE_PLAYLIST / NCM_QUEUE_SEARCH */
int appstate_get_queue(void);
int appstate_queue_count(void); /* 当前播放队列长度(按 queue_src) */
int appstate_queue_song(int idx, ncm_song_t *out); /* 队列第 idx 首;0=ok(按 queue_src) */
/* ---- 搜索结果独立数组(§23;g_search_mu 保护,UI/worker 经此访问)---- */
int appstate_search_count(int tab); /* tab: 0=单曲 1=歌单 */
int appstate_search_offset(int tab); /* 下次 offset(=已加载条数) */
int appstate_search_has_more(int tab); /* tab 是否还有下一页 */
int appstate_search_song(int i, ncm_song_t *out); /* 单曲条 i;0=ok */
int appstate_search_pl(int i, ncm_playlist_t *out); /* 歌单条 i;0=ok */
void appstate_search_clear(int tab); /* 清空某 tab 结果(offset=0, total=-1, more=1) */
void appstate_search_append_songs(int n, const ncm_song_t *s, int64_t total); /* 追加单曲(锁内) */
void appstate_search_append_pls(int n, const ncm_playlist_t *p, int64_t total); /* 追加歌单 */
/* ---- 搜索历史(≤NCM_SEARCH_HIST_MAX,最新在前;meow_config 持久化)---- */
int appstate_search_history_count(void);
int appstate_search_history_get(int i, char *out, size_t sz); /* 1=存在 */
void appstate_search_history_add(const char *kw); /* 去重 + 最新在前 + 截断 ≤12 */
void appstate_search_history_remove(int i); /* 删第 i 条 */
void appstate_search_history_clear(void);
/* account(app/logic/account.c,§20 会员态):account.json 落盘 + 刷新策略
* (登录后/缓存超 1 天/到期后 各查一次,不轮询——风控)。 */
void account_load(void); /* appstate_init 时读 account.json 恢复 vip 态 */
int account_vip(void); /* 当前有效会员档(到期自动降档视图,不联网) */
int account_level_cap(void); /* 会员态允许的最高音质档(NONE→EXHIGH/CVIP→HIRES/SVIP→SURROUND) */
void account_vip_maybe_refresh(int force); /* worker 线程上下文调用(含网络,单次) */
/* §NEW MUSIC_U 临期续期:App 启动(home_worker)调用——查 cookie 年龄,临期且登录且本 boot 未试
* 则后台静默刷新一次(用当前 cookie 换新登录态落盘);返回 NCM_OK(已续期/未临期/冷却中)、
* NCM_ERR_AUTH(会话失效,需回登录页,读 account_cookie_need_relogin)或负错误码(瞬断,退避)。 */
int account_cookie_maybe_refresh(void);
int account_cookie_need_relogin(void); /* UI 线程取「续期判失效需回登录页」标志 */
void account_cookie_relogin_consume(void); /* UI 线程消费该标志 */
void account_cookie_mark_fresh(void); /* 登录成功落盘新 cookie 后调用(重置年龄/单飞/冷却) */
void appstate_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
void autoplay_maybe(void); /* 真机联调 hook(/tmp/ncm_autoplay 触发,自动播已缓存曲目,一次性) */
int autoplay_ui_consume(void); /* UI 线程取走 autoplay 起播标志,返回曲目 idx(无则 -1) */
/* lyric */
int lyric_parse(const char *lrc, const char *tlrc); /* 填充 g_lyric_* */
int lyric_line_at(int64_t ms); /* 当前行 */
const char *lyric_line_text(int i);
const char *lyric_line_tl(int i);
int lyric_count(void);
void lyric_clear(void);
int lyric_state(void); /* 0=空闲/未拉 1=加载中 2=已拉完(可能 0 行=无词) */
long lyric_line_time(int i);/* 第 i 行时间戳(ms;越界 -1)。播放页点行跳转(§22) */
/* cover */
void cover_request(const char *url, const char *song_id); /* 提交目标封面 URL(后台线程下载+解码);song_id 曲目封面死链时 song/detail 兜底重取,NULL=歌单封面无兜底 */
meow_img *cover_get(void); /* 当前封面(**持锁返回**;NULL=无;用后必须 cover_put) */
void cover_put(void); /* 配对 cover_get 释放锁(§18 修绘制期 UAF) */
int cover_changed(void); /* 1=有新封面待绘制(UI tick 判变) */
void cover_ack(void); /* UI 消费一次(已见当前版本) */
const char *cover_current_path(void); /* 封面原始文件路径(供 launcher 媒体卡片读);NULL=无 */
void cover_cache_reset(void);
#ifdef MEOW_UI_HOST_TEST
void cover_host_inject(const char *path); /* 宿主测试桩:本地封面注入(验证封面/模糊底图) */
#endif
/* ---- play(app/play/)---- */
/* audio_out(后端:默认 SINK_AUDIOD=meow_audiod 独占服务;env NCM_AUDIO_SINK=file/null/alsa
* 或 MEOW_AUDIO_DIRECT=1 选调试后端,见 audio_out.c 文件头) */
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功;bits∈{16,32}(§19 flac>16bit→32) */
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);
/* player_manager:播放线程(下载+解码+输出)驱动 */
int player_manager_start(void); /* 起播放线程;0=成功 */
void player_manager_stop(void); /* 停播(清曲目/置 STOPPED) */
void player_manager_stop_thread(void);/* 停线程(app 退出时 join) */
void player_manager_play(int song_idx); /* 加载并播放 */
void player_manager_pause(int on); /* on=1 暂停 */
void player_manager_next(int dir); /* dir=+1/-1 */
void player_manager_seek(int64_t ms);
void player_manager_set_quality(int level, int cache_gb);
int player_manager_state(void); /* NCM_PC_* */
int player_manager_playing_song(void);
int64_t player_manager_pos_ms(void);
int64_t player_manager_dur_ms(void);
int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */
int player_manager_error(void);
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
void player_manager_dl(int64_t *done, int64_t *total); /* 本曲下载进度(字节;total=0 未知,§18) */
int player_manager_eff_level(void); /* 实际生效音质档(§20;-1=未定) */
/* 音质档位显示名/规格(ui/chrome.c,§20 八档 UI 权威口径;settings/player 共用) */
const char *ncm_level_name(int lv); /* 短名:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声 */
const char *ncm_level_spec(int lv); /* 规格:128kbps / ≤320kbps / 48kHz·16bit / ... */
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
int songmeta_load(const char *cache_dir, const char *song_id, ncm_song_t *out); /* NCM_OK / NCM_ERR_NOENT */
int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t *s);
/* 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 */
void app_host_paint_page(int page); /* 构建指定页并整帧绘制到 back_buf(供 dump)*/
#endif /* NCM_APP_INTERNAL_H */