595 lines
39 KiB
C
595 lines
39 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(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 ACT_QUEUE 6 /* 当前播放队列页(播放页「队列」钮进入,展示实时播放列表) */
|
||
|
||
/* ---- 播放模式三态(REPORT §NEW:随机 / 列表循环 / 单曲循环)----
|
||
* 默认=列表循环(延续既有「顺序下一首」语义;列表循环下自动顺延会在队尾回绕)。
|
||
* 三态按钮点击按 enum 序循环(SHUFFLE→LISTLOOP→SINGLE→SHUFFLE),图标/文案见 ncm_mode_name。 */
|
||
enum {
|
||
NCM_MODE_SHUFFLE = 0, /* 随机播放:自动播完/手动下一首 → 随机取队列另一首 */
|
||
NCM_MODE_LISTLOOP = 1, /* 列表循环:自动播完/手动下一首 → 顺序下一首(队尾回绕队首) */
|
||
NCM_MODE_SINGLE = 2, /* 单曲循环:当前曲播完 → 重播本曲;手动下一首仍顺序 */
|
||
};
|
||
#define NCM_MODE_COUNT 3
|
||
|
||
/* ---- 封面 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
|
||
|
||
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=APP_ROW_H)
|
||
* 不变——卡片在其行高内上下对称内缩 NCM_ROW_GAP/2,故文本(SDK 按 APP_ROW_H 垂直居中)仍落卡中心;
|
||
* 相邻卡顶间距恒=APP_ROW_H=卡高+NCM_ROW_GAP。要求偶数,保证上下对称。 */
|
||
#define NCM_ROW_GAP 8
|
||
/* 第 idx 行(行高 item,滚动 off,列表顶 list_y)的圆角卡:顶 y 与高 h。 */
|
||
static inline int ncm_row_card_y(int item, int idx, int off, int list_y)
|
||
{
|
||
return list_y + (idx * item - off) + NCM_ROW_GAP / 2;
|
||
}
|
||
static inline int ncm_row_card_h(int item) { return item - NCM_ROW_GAP; }
|
||
|
||
/* ---- 播放页(§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 play_mode; /* NCM_MODE_*(播放模式三态;UI 读写,播放线程切歌依此) */
|
||
int cur_song; /* 当前曲目在队列(queue_src)下标;-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 */
|
||
int play_mode; /* 播放模式(§NEW 三态):NCM_MODE_*;持久化 ncm.play_mode */
|
||
|
||
/* 搜索(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_play_queue_song(int idx);/* 从「当前实时播放队列」第 idx 首播放(保持当前 queue_src,§NEW) */
|
||
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);
|
||
|
||
/* mini 播放栏(ui/minibar.c,跨 Activity 常驻;非播放页 + 有活动播放会话时显示)*/
|
||
void mini_bar_init(void); /* 登记 CUSTOM 绘制物(main 建页后调) */
|
||
void mini_bar_shutdown(void); /* 注销(app 退出前) */
|
||
void mini_bar_tick(uint64_t now);/* 每帧:可见性/歌曲/暂停态/进度变化 → 失效栏带并重绘 */
|
||
int mini_bar_hit(int x, int y); /* app_on_tap 顶层先行拦截:1=消费(toggle/进播放页) */
|
||
|
||
/* queue(当前播放队列页,§NEW:展示实时播放列表/分页/当前曲高亮/点播切歌) */
|
||
meow_view *queue_view_create(void);
|
||
int queue_is_active(void);
|
||
int queue_on_tap(int x, int y);
|
||
void queue_swipe(int dir, int dist);
|
||
void queue_touch_down(int x, int y);
|
||
void queue_touch_move(int x, int y);
|
||
void queue_open(void); /* 进当前播放队列页 */
|
||
void queue_tick(uint64_t now); /* UI tick:检查队列 worker 完成并刷新 */
|
||
|
||
/* 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) */
|
||
void appstate_apply_play_mode(int m); /* 设 g_st.play_mode + 落盘(持久化「记住模式」,§NEW) */
|
||
|
||
/* ---- 搜索结果独立数组(§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=未定) */
|
||
int player_manager_play_mode(void); /* 当前播放模式(§NEW 三态:NCM_MODE_*) */
|
||
void player_manager_set_play_mode(int m); /* 设播放模式(运行时态;持久化由 appstate_apply_play_mode 落盘) */
|
||
void player_manager_next_intent(void); /* 曲目自然播完 → 按播放模式决定下一首/重播(单曲循环重播本曲) */
|
||
|
||
/* 音质档位显示名/规格(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 / ... */
|
||
const char *ncm_mode_name(int m); /* 播放模式短名(§NEW 三态):随机 / 列表 / 单曲 */
|
||
|
||
/* 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=已消费一次) */
|
||
/* 批3:上报当前曲歌词(lrc 原文 + tlrc 翻译)到 launcher 媒体会话(MSG_META_LYRIC 分片)。
|
||
* 由 lyric_worker 在拉到/清一歌词后调(换歌/换词 seq 自增;lrc/tl 可为空=清词)。 */
|
||
void ipc_media_lyric_report(const char *lrc, const char *tl);
|
||
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 */
|