Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/ui/player.c
T
MeowOS 997f89f879 ncm 三期三批: 歌曲对象瘦身(1324→608B)+滑动窗口(NCM_SONG_WINDOW=256, 取消1000截断支撑任意长度歌单)+播放快照解耦 — agent-251
- core/ncm_api.h: ncm_song_t 瘦身 id24/name128/art96/al96/pic256/dt int32 (1324B→608B)
- appstate.c 滑动窗口: song_ids 全量 trackIds 索引(16384) + songs[256] 窗口感知
  (song_valid 位图/window_base/滑窗 memmove 保留重叠), ensure_range 批网格(100)对齐
  预取, songmeta.c 盘缓存 <cache_dir>/meta/<id>.bin (回滚/二次进入 0 网络请求)
- playlist.c 触底分页退役→视口 ensure(±24 行), 占位行可直点(播放线程 fetch 物化),
  footer 改视口区间/总数
- 播放兼容: g_play.play_song 快照(mu), run_song 起播物化后全链路(下载/歌词/IPC META/
  播放页 UI)读快照——窗口滑出当前播放曲不崩; 边界 song_count→track_ids_count
- 验证(0 真实请求): pagination_sim 全 PASS(5191 无截断/52 批); window_sim 6000 首
  全程下滚 750 步视口全物化/song_detail 恰 60 次/回滚+0 请求/播放 fetch 不滑窗;
  core 宿主+qemu 全 PASS; mipsel -Wall -Wextra 0 warning, bss 2052068→1080900(-47%)
- 未部署真机(设备音频线排查中, 部署留主会话)
2026-09-01 04:03:01 +08:00

430 lines
18 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.
/* ui/player.c — 播放页(ACT_PLAYER):黑胶唱盘 + 唱针 + 进度/控制/滚动歌词(竖/横布局可切)。
*
* 黑胶(NCM_UI_DESIGN §5.1,软渲无圆形原语,全 fill_rect 行扫):
* ① 盘体 = 对每行 dy 取 half=√(R²-dy²) 画 fill_rect(同心圆)。
* ② 封面粉圆 = 方盖(S=R×√2)内切于圆 + 四角把「圆外-方外」两段盖回盘色 = 封面成圆。
* ③ 唱针 = 从盘心右上到盘缘一根 3px 唱臂 + 端头;播放压盘/暂停微抬(状态切换才画)。
* 盘恒黑(palette.disc),accent(进度已播/播放钮/歌词高亮)日夜换红明暗(palette.red/red_lit)。
*
* 性能(§6,专项):**分带刷新**——内容拆 盘带(仅圆盘) + 动态带(歌名/进度/控制/歌词/页脚) 两个绘制物,
* 各带独立且互不重叠 bbox。tick 只标脏「变化的那一条带」,盘带仅 歌曲/封面/播停态 变化才重画
* (修 player_tick 每 tick 刷整 r_cont 的隐患)。播放页恒沉浸(day 瓷白 / night 近黑,
* 不随系统明暗整体反转,仅 accent 换红明暗,§5.4)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
typedef struct { meow_view *view; int active; } player_ctrl;
static player_ctrl g_plc;
static meow_widget *g_tb;
static ncm_rect_t r_cont;
static int g_custom_disc, g_custom_dyn;
static int g_landscape;
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song;
static char g_last_meta_id[24]; /* 已上封面的元数据 id(快照滞后 settle 时纠正封面/歌名) */
/* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
static ncm_rect_t g_disc_r, g_dyn_r; /* 盘带 / 动态带 bbox(不重叠) */
static int g_cx, g_cy, g_R, g_S; /* 盘心 / 半径 / 封面边长 */
static ncm_rect_t g_bar; /* 进度条 */
static int g_times_y;
static int g_ctl_y, g_cprev_x, g_cplay_x, g_cnext_x, g_cr;
static int g_title_y, g_artist_y, g_info_x, g_info_w;
static ncm_rect_t g_lyr; /* 歌词区 */
#define CTL_RR 30 /* 侧钮命中半径(命中容差) */
static int cur_song(void)
{
int c = player_manager_playing_song();
return (c >= 0 && c < g_st.track_ids_count) ? c : -1;
}
/* 当前播放曲元数据(§17):优先播放快照(窗口无关);快照未就绪时退窗口拷贝。 */
static int cur_song_meta(ncm_song_t *out)
{
if (player_manager_current_song(out) == 0) return 0;
int sc = cur_song();
if (sc >= 0 && appstate_song_copy(sc, out) == 0) return 0;
return -1;
}
static void fmt_ms(int64_t ms, char *out, size_t sz)
{
if (ms < 0) ms = 0;
int sec = (int)(ms / 1000);
snprintf(out, sz, "%d:%02d", sec / 60, sec % 60);
}
static void tri(int x0, int y0, int h, int w, int dir, uint32_t color)
{
for (int i = 0; i < h; i++) {
int wi = 1 + i * w / h;
if (dir == 1) meow_ui_fill_rect(x0, y0 + i, wi, 1, color);
else meow_ui_fill_rect(x0 - wi + 1, y0 + i, wi, 1, color);
}
}
static void draw_ctl(int cx, int cy, int r, int kind, uint32_t base, uint32_t fg)
{
for (int i = 0; i < 2 * r; i++) {
int dy = i - r;
int half = (int)sqrt((double)(r * r - dy * dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy - r + i, 2 * half, 1, base);
}
int t = r / 2;
if (kind == 0) { meow_ui_fill_rect(cx - r - 2, cy - t, 4, 2*t, fg);
tri(cx + r - 4, cy - t, 2*t, 2*t, -1, fg); }
else if (kind == 1) { tri(cx - t, cy - t, 2*t, 2*t, 1, fg); }
else if (kind == 2) { meow_ui_fill_rect(cx - t - 2, cy - t, 4, 2*t, fg);
meow_ui_fill_rect(cx + t - 2, cy - t, 4, 2*t, fg); }
else { tri(cx - r + 4, cy - t, 2*t, 2*t, 1, fg);
meow_ui_fill_rect(cx + r - 2, cy - t, 4, 2*t, fg); }
}
static void arm_line(int x0, int y0, int x1, int y1, uint32_t color, int wpx)
{
int dx = x1 - x0, dy = y1 - y0;
int steps = (abs(dx) > abs(dy) ? abs(dx) : abs(dy)) / 2 + 1;
if (steps < 1) steps = 1;
for (int i = 0; i <= steps; i++) {
int x = x0 + dx * i / steps;
int y = y0 + dy * i / steps;
meow_ui_fill_rect(x - wpx / 2, y - wpx / 2, wpx, wpx, color);
}
}
/* 几何(竖/横布局)。带 bbox:盘带=圆盘矩形,动态带=其余(互不重叠)。 */
static void player_compute_geom(void)
{
r_cont = ncm_content_rect();
if (g_landscape) {
g_cx = 180; g_cy = 360; g_R = 120; g_S = (int)(g_R * 1.414);
g_info_x = 340; g_info_w = r_cont.x + r_cont.w - 16 - g_info_x;
g_title_y = 150; g_artist_y = 188;
g_bar = NR(g_info_x, 240, 108, 4); g_times_y = 226;
g_ctl_y = 310; g_cprev_x = 348; g_cplay_x = 404; g_cnext_x = 460; g_cr = 26;
g_lyr = NR(300, 372, 164, 280);
} else {
g_cx = 240; g_cy = 272; g_R = 104; g_S = (int)(g_R * 1.414);
g_info_x = 240; g_info_w = r_cont.w;
g_title_y = 388; g_artist_y = 422;
g_bar = NR(56, 456, 368, 4); g_times_y = 436;
g_ctl_y = 508; g_cprev_x = 176; g_cplay_x = 240; g_cnext_x = 304; g_cr = 30;
g_lyr = NR(16, 546, 448, 150);
}
int cb = r_cont.y + r_cont.h - APP_FOOTER_H;
if (g_lyr.y + g_lyr.h > cb) g_lyr.h = cb - g_lyr.y;
if (g_lyr.h < 0) g_lyr.h = 0;
if (g_landscape) {
g_disc_r = NR(g_cx - g_R, g_cy - g_R, 2*g_R, 2*g_R); /* 左盘区 */
int dy0 = g_title_y - 12;
g_dyn_r = NR(g_info_x - 20, dy0, r_cont.x + r_cont.w - (g_info_x - 20), cb - dy0 + 8);
} else {
g_disc_r = NR(0, g_cy - g_R, r_cont.w, 2*g_R); /* 全宽圆盘带 */
g_dyn_r = NR(0, g_cy + g_R, r_cont.w, cb - (g_cy + g_R)); /* 其余(含歌名/进度/歌词) */
}
}
/* 盘带绘制物:盘体 + 方盖内切圆(四角盖色) + 封面 + 唱针。仅 歌曲/封面/播停态 见变化才重画。 */
static void plr_draw_disc(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
int cx = g_cx, cy = g_cy, R = g_R, S = g_S;
int st = player_manager_state();
int paused = (st == NCM_PC_PAUSED);
for (int dy = -R; dy <= R; dy++) {
int half = (int)sqrt((double)(R*R - dy*dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->disc);
}
for (int rr = R - 3; rr >= R - 12; rr -= 4) {
for (int dy = -rr; dy <= rr; dy++) {
int half = (int)sqrt((double)(rr*rr - dy*dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->disc_edge);
}
}
/* 封面:方盖内切于圆 + 四角盖回盘色 → 封面成圆。 */
meow_img *cov = cover_get();
int hS = S / 2;
if (cov) meow_ui_img_cover(cx - hS, cy - hS, S, cov);
else {
for (int dy = -hS; dy <= hS; dy++) {
int half = (int)sqrt((double)(hS*hS - dy*dy));
if (half <= 0) continue;
meow_ui_fill_rect(cx - half, cy + dy, 2*half, 1, pa->red_ctr);
}
meow_ui_text_center(cx, cy - 10, "\u266a", th->font_body, pa->red);
}
for (int dy = -R; dy <= R; dy++) {
int half = (int)sqrt((double)(R*R - dy*dy));
if (half <= hS) continue;
meow_ui_fill_rect(cx - half, cy + dy, half - hS, 1, pa->disc);
meow_ui_fill_rect(cx + hS, cy + dy, half - hS + 1, 1, pa->disc);
}
meow_ui_fill_rect(cx - 2, cy - 2, 4, 4, pa->player_bg); /* 盘心孔 */
/* 唱针:播放压盘 / 暂停微抬 6px。 */
int arm_top = paused ? 6 : 0;
int ax0 = cx + 40, ay0 = cy - 70 + arm_top;
int ax1 = cx + R - 6, ay1 = cy + 8 + arm_top;
arm_line(ax0, ay0, ax1, ay1, th->outline, 3);
meow_ui_fill_rect(ax1 - 3, ay1 - 3, 6, 6, th->onSurface);
/* 高光(廉价):左下 45° 1px 弧。 */
for (int a = 0; a < 20; a++) {
double t = (a / 19.0) * (3.14159 / 2);
int hx = cx - (int)((R - 6) * 0.71 * cos(t));
int hy = cy + (int)((R - 6) * 0.71 * sin(t));
meow_ui_fill_rect_alpha(hx, hy, 2, 1, 0xFFFFFF, 60);
}
}
/* 动态带绘制物:歌名/歌手 + 进度条/时间 + 控制钮 + 滚动歌词 + 页脚。tick 主要刷此带。 */
static void plr_draw_dyn(void *ctx)
{
(void)ctx;
if (meow_ui_activity_active() != ACT_PLAYER) return;
const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark);
ncm_song_t sm;
int have = (cur_song_meta(&sm) == 0);
const char *nm = have ? sm.name : "暂无播放";
const char *ar = have ? sm.art_name : "";
if (g_landscape) {
meow_ui_text(g_info_x, g_title_y, ncm_text_fit(nm, g_info_w, 28), 28, th->onSurface);
meow_ui_text(g_info_x, g_artist_y, ncm_text_fit(ar, g_info_w, 20), 20, th->onSurfaceVariant);
} else {
meow_ui_text_center(g_info_x, g_title_y, nm, 28, th->onSurface);
meow_ui_text_center(g_info_x, g_artist_y, ar, 20, th->onSurfaceVariant);
}
/* 进度条 + 时间 */
int pos = (int)player_manager_pos_ms();
int dur = (int)player_manager_dur_ms();
if (dur <= 0 && have) dur = (int)sm.duration_ms;
int frac = dur > 0 ? (int)((int64_t)pos * 1000 / dur) : 0;
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
char lp[16], rp[16];
fmt_ms(pos, lp, sizeof lp);
fmt_ms(dur, rp, sizeof rp);
if (g_landscape) {
meow_ui_text(g_bar.x, g_times_y, lp, 20, th->onSurfaceVariant);
meow_ui_text_right(g_bar.x + g_bar.w, g_times_y, rp, 20, th->onSurfaceVariant);
} else {
meow_ui_text(44, g_times_y, lp, 20, th->onSurfaceVariant);
meow_ui_text_right(r_cont.x + r_cont.w - 44, g_times_y, rp, 20, th->onSurfaceVariant);
}
meow_ui_fill_rect(g_bar.x, g_bar.y, g_bar.w, g_bar.h, th->surfaceVariant);
meow_ui_fill_rect(g_bar.x, g_bar.y, (int)((int64_t)g_bar.w * frac / 1000), g_bar.h, pa->red);
int st = player_manager_state();
draw_ctl(g_cprev_x, g_ctl_y, g_cr - 6, 0, th->surfaceContainer, th->onSurface);
draw_ctl(g_cplay_x, g_ctl_y, g_cr, (st == NCM_PC_PAUSED) ? 1 : 2, pa->red, pa->on_red);
draw_ctl(g_cnext_x, g_ctl_y, g_cr - 6, 3, th->surfaceContainer, th->onSurface);
/* 歌词区(分隔线 + 滚动歌词,当前行红高亮居中) */
meow_ui_fill_rect(g_lyr.x, g_lyr.y, g_lyr.w, 1, th->outlineVariant);
int n = lyric_count();
int line_h = (g_landscape) ? 24 : 26;
int center_y = g_lyr.y + g_lyr.h / 2;
int lcx = g_lyr.x + g_lyr.w / 2;
if (n == 0) {
/* 二期:歌词后台加载,状态区分「加载中 / 真的无词」 */
meow_ui_text_center(lcx, center_y - 10,
lyric_state() == 2 ? "暂无歌词" : "歌词加载中…",
20, th->onSurfaceVariant);
}
else {
int64_t ps = player_manager_pos_ms();
int cur2 = lyric_line_at(ps);
int lw = g_lyr.w - 8; /* 歌词列宽(裁剪,防窄列溢出) */
for (int i = cur2 - 6; i <= cur2 + 5; i++) {
if (i < 0 || i >= n) continue;
int yy = center_y + (i - cur2) * line_h;
if (yy < g_lyr.y || yy + line_h > g_lyr.y + g_lyr.h) continue; /* 裁剪到歌词带(自定义绘制物不按 bbox 裁剪) */
int is_cur = (i == cur2);
meow_ui_text_center(lcx, yy, ncm_text_fit(lyric_line_text(i), lw, is_cur ? 24 : 20),
is_cur ? 24 : 20, is_cur ? pa->red : th->onSurfaceVariant);
const char *tl = lyric_line_tl(i);
if (is_cur && tl && tl[0])
meow_ui_text_center(lcx, yy + 24, ncm_text_fit(tl, lw, 20), 20, th->secondary);
}
}
/* 页脚提示(用「・」U+30FB 而非 U+00B7「·」:common 字库按 GB2312 解码 A1A4 得 U+30FB,
* 不含 U+00B7 → 后者在设备上是豆腐块。REPORT §11 二期。) */
char foot[64];
snprintf(foot, sizeof foot, "左缘右滑返回 ・ ⟳ 切横屏");
meow_ui_text_center(r_cont.x + r_cont.w / 2, r_cont.y + r_cont.h - APP_FOOTER_H,
foot, 20, th->onSurfaceVariant);
}
int player_is_active(void) { return g_plc.active; }
int player_screen(void) { return g_landscape; }
int player_set_screen(int landscape)
{
int old = g_landscape;
g_landscape = landscape ? 1 : 0;
player_compute_geom();
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
return old;
}
void player_on_tap(int x, int y)
{
if (g_tb) {
int ah = meow_topappbar_hit(g_tb, x, y);
if (ah == MEOW_TOPBAR_BACK) { meow_view_page_pop(); return; }
if (ah >= 0) { player_set_screen(!g_landscape); return; }
}
if (x >= g_cplay_x - g_cr && x <= g_cplay_x + g_cr && y >= g_ctl_y - g_cr && y <= g_ctl_y + g_cr) {
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
else player_manager_pause(1);
return;
}
if (x >= g_cprev_x - CTL_RR && x <= g_cprev_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
player_manager_next(-1); return;
}
if (x >= g_cnext_x - CTL_RR && x <= g_cnext_x + CTL_RR && y >= g_ctl_y - CTL_RR && y <= g_ctl_y + CTL_RR) {
player_manager_next(1); return;
}
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
int64_t dur = player_manager_dur_ms();
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
return;
}
}
void player_play_song(int idx)
{
if (idx < 0 || idx >= g_st.track_ids_count) return;
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。 */
ncm_song_t s;
const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0])
? s.al_pic : g_st.cover_url;
player_manager_play(idx);
appstate_set_playing_song(idx, cover);
cover_request(cover);
meow_view_page_push(ACT_PLAYER);
}
void player_pause_toggle(int on)
{
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
else player_manager_pause(1);
(void)on;
}
void player_next(int dir) { player_manager_next(dir); }
void player_seek(int64_t ms) { player_manager_seek(ms); }
void player_on_swipe(int dir, int dist) { (void)dir; (void)dist; }
/* tick:分带刷新——只标脏变化的那一带(§6)。盘带仅在歌曲/封面/播停态变化时重画。 */
void player_tick(uint64_t now)
{
(void)now;
chrome_tick();
if (!g_plc.active) return;
int sc = cur_song();
int pos = (int)player_manager_pos_ms();
int st = player_manager_state();
int line = (lyric_count() > 0) ? lyric_line_at(pos) : -1;
if (sc != g_last_song) {
g_last_song = sc;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌名/歌手变 */
}
/* 元数据 id 变化(含快照滞后 settle:切歌瞬间快照仍是旧曲,play 线程更新后此处纠正)→
* 请求新曲专辑封面(黑胶盘换图)+ 刷动态带。 */
ncm_song_t s2;
if (sc >= 0 && cur_song_meta(&s2) == 0 && strcmp(s2.id, g_last_meta_id) != 0) {
ncm_strlcpy(g_last_meta_id, s2.id, sizeof g_last_meta_id);
cover_request(s2.al_pic[0] ? s2.al_pic : g_st.cover_url);
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h);
}
if (cover_changed()) {
cover_ack();
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
}
if (pos != g_last_pos_ms || st != g_last_state) {
g_last_pos_ms = pos;
int state_changed = (st != g_last_state);
g_last_state = st;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 进度/控制钮/时间 */
if (state_changed)
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h); /* 唱针抬/压 */
}
if (line != g_last_line) {
g_last_line = line;
meow_ui_invalidate(g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h); /* 歌词 */
}
}
static void plr_page_create(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->view = v;
player_compute_geom();
meow_ui_activity_push(ACT_PLAYER); /* owner=ACT_PLAYER */
g_tb = ncm_topbar_create("正在播放", 1, "\xe2\x86\xbb"); /* 返回 + ⟳ 布局 */
meow_ui_activity_pop();
g_custom_disc = meow_draw_custom_register(10, g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h,
plr_draw_disc, NULL);
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h,
plr_draw_dyn, NULL);
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1;
g_last_meta_id[0] = 0;
}
static void plr_page_enter(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->active = 1;
const ncm_pal_t *pa = ncm_palette(g_dark);
meow_ui_set_page_bg(pa->player_bg); /* 播放页恒沉浸,不随系统明暗整体反转 */
meow_ui_paint_full();
if (!g_host_mock) player_manager_load_lyric();
}
static void plr_page_leave(meow_view *v, void *ctx)
{
(void)v;
((player_ctrl *)ctx)->active = 0;
}
static void plr_page_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_custom_disc) { meow_draw_unregister(g_custom_disc); g_custom_disc = 0; }
if (g_custom_dyn) { meow_draw_unregister(g_custom_dyn); g_custom_dyn = 0; }
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
}
meow_view *player_view_create(void)
{
static const meow_view_hooks h = {
.on_create = plr_page_create,
.on_enter = plr_page_enter,
.on_leave = plr_page_leave,
.on_destroy = plr_page_destroy,
};
return meow_view_create(ACT_PLAYER, MEOW_VIEW_ACTIVITY, &h, &g_plc);
}