/* ncm_app_internal.h — dev.player.ncm 应用内部共享头(UI 各页 + app 装配层 + 宿主测试面)。 * * 层级(对齐 hello_app/app.c 批② + files_app): * - app.c main + 装配 + 事件分发(把 meow_ui_run 的平铺回调路由到各页) * - app/ui/.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 #include #include #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 §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); * 滚动出窗的槽位失效(数据已序列化到 /meta/.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 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 获取中) */ /* 播放曲快照(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]; /* 我的歌单 */ 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; /* 搜索(结果复用 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/.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); /* 兼容:载索引 + 首个窗口(内部走 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) 已物化。 * 窗口未覆盖先滑窗(重叠区保留,出窗槽位失效); * 缺失槽位先读盘缓存(/meta/.bin), * miss 攒批 ≤NCM_SONG_BATCH 走 song_detail,成功落盘。 * 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(播放上下文) */ 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 行=无词) */ /* 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(后端:默认 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=成功 */ int audio_out_write(const short *samples, int frames); /* <0=错误/结束 */ 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) */ /* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(/meta/.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 */