Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ncm_app_internal.h
T
MeowOS ee104f1b18 feat(ncm): 三期第一批 — 歌单真分页(取消256截断)+专辑封面ncm_cache+歌词后台加载补锁
[agent-239] 仅改 patch_v2/ncm_app/。

1. 歌单分页(REPORT §14.1): track/all 全 404(§13.1) 弃用, 回 detail(n=1)全量 trackIds
   + song_detail 分批(≤NCM_SONG_BATCH=100) 触底逐页加载。appstate_load_playlist_songs_page
   offset=0 重建 song_ids[]/track_ids_count 载第0页, offset>0 追加(失败保留已载);
   playlist.c g_pl_more=(song_count<track_ids_count && <NCM_SONG_MAX), 触底 g_ploffset=song_count
   起下一页(保留滚动), 首屏失败整页空态+点按重试, 追加失败 snack+保留已载。NCM_SONG_MAX
   256→1000(单页渲染上限, bss +~1MB), NCM_SONG_BATCH 64→100, 删 NCM_PLAYLIST_PAGE(track/all 遗留)。

2. 专辑封面(§14.2): cover.c 封面改走 ncm_cache(LRU, 键=djb2(url)→song_cov<h>.jpg 同池),
   缓存命中免下载/未命中下载→open/write/commit 落盘, 仍写 cover_current.jpg 供 launcher;
   顺带修真 bug: save_cover_raw 在已持 g_cm 临界区内再 lock(非递归死锁)→封面永不刷新, 改调用方持锁。
   歌单列表/详情头块封面保持现状。

3. 歌词后台加载(§14.3): lyric_worker 已异步(§11)未改线程模型; 补 lyric.c g_lyric_mu 数据竞争锁,
   写侧 lyric_parse 整段持锁(清空用 lyric_clear_nolock 免二次取锁), 读侧 lyric_count/line_at/
   line_text/line_tl 逐次取锁, 消除 UI/IPC 与 worker 间数据竞争(原 §10.9 FM4 良性竞态)。

验证(0 真实 NCM 请求): bench/pagination_sim.c 离线 stub 驱动真实 appstate 分页函数全 PASS
(130首读全程/5191首封顶1000/空/越界无操作); mipsel build_ncm.sh 0 warning(text 842820/bss 2052068);
宿主 build_ncm_host.sh+MEOW_UI_DUMP home/playlist/player/lands 渲染正常无崩溃; host SDK_SRC 补
meow_pinyin(拼音IME py_predict 漏编致链接断)。ncm md5 b34bc1ed, ncm_host md5 1a2327e0。
未动 sdk/(主会话改 meow_ui_ime.c)/meow_launcherd/meow_amsd/vendor/core; 未 commit 它们。
2026-08-31 19:33:10 +08:00

321 lines
16 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(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 "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
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
/* 单个歌单曲目上限 = 分页驱动下的「单页渲染上限」(REPORT §14,三期第一批):
* detail(/v6/playlist/detail, n=1)取回全量 trackIds 入 song_ids[](≤ NCM_SONG_MAX);
* song_detail 按 NCM_SONG_BATCH 分批拉歌词对象,列表触底自动加载下一页,直到
* song_count == track_ids_count(歌单尾部)或 == NCM_SONG_MAX(内存上限,footer 显示 x/总数)。
* 不再硬截断 256(NCM_SONG_MAX 从 256 提升到 1000,约 1.32KB/首 → 歌曲数组 ~1.37MB,14MB 内安全)。
* 数值权衡见 REPORT §14.8(可再上调,代价是 bss 线性增长:每 +100 首 ≈ +132KB)。 */
#define NCM_SONG_MAX 1000 /* 歌单曲目/歌曲对象上限(append-only 分页加载) */
#define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
* 同时作「下一页」加载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
/* ---- 播放/媒体会话状态(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,
};
/* 跨线程播放运行时状态(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];
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
} 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];
/* 我的歌单 */
ncm_playlist_t playlists[NCM_PLAYLIST_MAX];
int playlist_count;
int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_SONG_MAX,分页索引);
* songs[] = 已物化的歌曲对象(append-only,与 song_ids[] 前 song_count 首一一对应);
* song_count = 已物化数(列表 count = song_count);track_ids_count = trackIds 总数(≥ song_count,
* 其差值 = 仍有「下一页」待 song_detail 分批拉取)。 */
int64_t song_ids[NCM_SONG_MAX];
ncm_song_t songs[NCM_SONG_MAX];
int song_count;
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_SONG_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;
/* 搜索(结果复用 g_st.songs[] 浅拷贝区;仅搜索页上下文有效)*/
char search_query[256];
int search_count; /* 当前搜索结果数(含 0=无/未搜) */
volatile int search_loading; /* 1=搜索请求在途 */
int search_err; /* 最近一次搜索错误(0=ok) */
} 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_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 首开始播放 */
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); /* 横屏切换/返回 */
int player_set_screen(int landscape); /* 1=横屏 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 完成并刷新 */
/* ---- 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); /* 兼容:整批载入(内部走分页循环) */
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
/* detail(全量 trackIds)后按页拉 song_detail:
* offset=0 重头(重建 song_ids[]/track_ids_count,清空后载第 0 页);
* offset>0 追加第 offset 页到 g_st.songs[offset..];
* 写 g_st.song_count(=offset+本页实际数)。 */
void appstate_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
/* 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 行=无词) */
/* cover */
void cover_request(const char *url); /* 提交目标封面 URL(后台线程下载+解码) */
meow_img *cover_get(void); /* 当前封面(不拥有;NULL=无) */
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);
/* ---- play(app/play/)---- */
/* audio_out */
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功 */
int audio_out_write(const short *samples, int frames); /* <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);
/* 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 */