[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 它们。
321 lines
16 KiB
C
321 lines
16 KiB
C
/* 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 */
|