Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ncm_app_internal.h
T
AskaEth 68ce46a5d8 fix(ncm §32): 内存瘦身修 OOM 被杀 —— 封面降采样(8.28MB→0.78MB) + 瞬态 rbuf 缩减 + 线程栈显式
真机 dmesg 实锤:OOM Kill ncm (score 460, anon-rss 26.9MB, 设备 56MB)。
画像:封面全分辨率 RGBA 8.28MB(1448x1448 原生解码驻留) 是最大可控块。

瘦身(按收益排序):
1. sdk meow_ui_img 新增纯增量 meow_img_downscale + cover.c cover_cap(≤480px)
   → 封面 8.28MB→0.78MB,-7.65MB(mem_sim 实测 PASS)。
2. ncm_api.c 瞬态响应 rbuf:song_url→64KB、lyric/song_detail/cloudsearch/user_playlist→256KB
   (detail/track_all 保 1MB)。
3. ncm_app_internal.h 新增 ncm_thread_create(显式 128KB 栈),替换全部 14 处 pthread_create。
4. 滑动窗口/meta 复核零回退(window_sim:608B/首,6000 首 280KB 恒定)。

验证:mem_sim 封面收益 PASS + host home/player 渲染 OK + stream_sim/player_flow_sim/
window_sim/search_sim/cache_evict_sim 全 PASS + test_ncm_core host+mipsel(qemu) PASS
+ build_ncm.sh -Wall -Wextra 0 warning。真机 scp 播放在跑确认(缓存曲,0 播放请求)。

新 ncm md5 44fa16fe(text 1015150/bss 1362500)。正式部署留主会话。
2026-09-02 03:43:42 +08:00

504 lines
31 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 /* 搜索页(顶栏搜索按钮进入) */
/* ---- 布局 ---- */
#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 恒发此;按当前态翻转) */
};
/* 跨线程播放运行时状态(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 缓冲菊花) */
/* 播放曲快照(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_checked_at; /* vip/info 最近获取时间(秒) */
/* 我的歌单 */
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 线程上下文调用(含网络,单次) */
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 触发,自动播已缓存曲目,一次性) */
/* 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); /* 提交目标封面 URL(后台线程下载+解码) */
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);
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);
/* ---- 宿主测试面(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 */