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%)
- 未部署真机(设备音频线排查中, 部署留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 04:03:01 +08:00
1 parent d8898afa09
commit 997f89f879
18 files changed
+987 -209

No files matched your search

+161
View File
@@ -1293,3 +1293,164 @@ db5b78ad9e8f40e6306484efe2a08eb3 ncm # mipsel 静态 App
- 设备日志中 ipc_media META 每秒一对(state=0 空态也发)为既有行为,本批未动。 - 设备日志中 ipc_media META 每秒一对(state=0 空态也发)为既有行为,本批未动。
- **未碰** /data/recovery_all、未 killall hiby_player、未主动重启设备(§16.3 的多次重启均为 - **未碰** /data/recovery_all、未 killall hiby_player、未主动重启设备(§16.3 的多次重启均为
被测路径触发的设备自重启)。 被测路径触发的设备自重启)。
---
## 17. 三期第三批:歌曲对象瘦身 + 滑动窗口(任意长度歌单,取消 1000 截断)+ 播放快照(2026-09-01)
> 范围:仅 `patch_v2/ncm_app/`(含 `core/` 结构体瘦身)。未动 `sdk/`、`meow_launcherd/`、
> `meow_audiod/`、`vendor/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub + /tmp 盘缓存)。
> **本批未部署真机**(设备音频线挂死排查中,ncm 二进制不动卡;部署留主会话)。
### 17.1 动机
§14 的歌单分页(detail 全量 trackIds 驱动 song_detail 分批 100、触底加载)仍受
`NCM_SONG_MAX=1000` 内存硬上限约束(每首 `ncm_song_t` ≈1324B → 1000 首 ≈1.32MB bss),
用户「我喜欢」6000+ 首只能看到前 1000。本批把「全量物化」改为**滑动窗口**:内存只保留
视口附近的歌曲对象(256 首),滚动出窗释放、入窗按需拉取,支撑任意长度歌单。
### 17.2 歌曲对象瘦身(core/ncm_api.h,1324B → 608B,-54%)
| 字段 | 旧 | 新 | 依据 |
|---|---|---|---|
| `id` | 32 | 24 | 数字文本,int64 最大 19 位 + NUL |
| `name` | 256 | 128 | 列表/播放页展示均按宽再截断(`ncm_text_fit`/listview) |
| `art_name` | 256 | 96 | 首个歌手名 |
| `album_name` | 256 | 96 | 同上 |
| `al_pic` | 512 | 256 | picUrl 实际 <200 字符;`obj_str` 截断安全 |
| `duration_ms` | `long` | `int32_t` | <2^31ms≈24 天;MIPS32 的 long 本就 32 位(零行为变化) |
解析侧(`parse_songs_array`)全部走 `sizeof 字段` 拷贝,无需改动;core 测试
(`test_ncm_core.c` 断言 id/name/ar/al/dt/fee/al_pic)宿主 + qemu-mipsel 全 PASS。
### 17.3 滑动窗口(appstate.c + playlist.c)
- **内存模型**(`ncm_app_internal.h`):
- `song_ids[NCM_TRACKID_MAX=16384]` = detail 取回的全量 trackIds 索引(int64,128KB bss,
不再 1000 截断;16384×~13B ≈ 213KB JSON,远在 2MB 响应限幅内);
- `songs[NCM_SONG_WINDOW=256]` + `song_valid[256]` 位图 = 物化窗口(256×608B≈152KB bss),
`songs[i] ↔ 全局下标 window_base+i`;
- 列表 count = `track_ids_count`(全量),未物化行画占位「··· / 加载中」。
- **API**(`app/logic/appstate.c`,全锁 `g_song_mu`;网络/盘 IO 在锁外,锁内仅内存搬移):
- `appstate_song_ensure_range(lo,hi)`:窗口未覆盖先滑窗(重叠区 `songs+valid` 一起
memmove 保留,出窗槽位失效),缺失槽先读盘缓存,miss 攒批 song_detail 补载并落盘;
- `appstate_song_copy(gidx,&out)` / `appstate_range_loaded(lo,hi)`:锁内读窗(UI 行回调);
- `appstate_song_fetch(gidx,&out)`:任意下标物化(窗→盘→song_detail 单曲),**不滑窗**
(播放线程专用,避免与浏览视口抢窗口造成乒乓);
- `appstate_adopt_songs(n)`:收编外部已填的 `songs[0..n)`(搜索结果)为窗口 base=0 播放上下文;
- 兼容入口 `appstate_load_playlist_songs_page(idx,offset,limit)`:offset=0 重建索引+物化
首段;offset>0 = ensure [offset,offset+limit)。
- **批网格对齐预取**:补载区间对齐到 `NCM_SONG_BATCH=100` 的全局网格([a0,b0) ≤100),
逐行滚动不产生小请求放大——6000 首全程下滚 song_detail 调用恰 = 60 次(6000/100)。
- **盘缓存(app/logic/songmeta.c,新)**:拉过的 song_detail 结果逐首序列化到
`<cache_dir>/meta/<id>.bin`(头 magic/ver/size + 结构体镜像;ver/size 失配判 miss)。
刻意不进 ncm_cache LRU 池:meta 每首 ≈600B(一万首 ≈6MB),不被清音频缓存误清。
效果:二次进入歌单 / 回滚全程 **0 网络请求**(离线可浏览已拉过的部分)。
- **UI(playlist.c)**:`pl_maybe_load_more`(触底追加)退役 → `pl_maybe_ensure`(视口 ±24 行
有未物化槽 → 后台 worker ensure);footer 改「视口区间 / 总数」(如 `5965-5988 / 6000 首`);
首屏失败整页空态 + 点按重试不变;补载失败 snack + 占位行保留(滚动/点按再试)。
### 17.4 播放兼容(窗口滑出当前播放曲不许崩)
- **播放快照**:`g_play.play_song`(mu 保护)——`run_song` 起播时物化目标曲
(`song_copy` 窗内命中 / `song_fetch` 窗外)并整体拷入快照;之后下载(id)、时长、
歌词 worker(id)、IPC META(title/artist/album)、播放页 UI(歌名/歌手/封面)**全读快照**,
与窗口完全解耦。窗口滑出当前播放曲 → 播放/歌词/媒体会话照常。
- 上下首/自然顺延边界:`g_st.song_count`(窗口内有效数)→ `g_st.track_ids_count`(全量索引),
cur_song 语义 = 全局下标不变。
- **占位行可直接点歌**:`player_play_song(row)` 不要求行已物化——封面回落歌单封面,
元数据由播放线程 fetch 后快照接管;UI tick 按快照 id 变化纠正封面/歌名(快照滞后 settle)。
- 搜索上下文:`search_worker` 结果经 `adopt_songs` 收编(song_ids 同步 strtoll),
播放/上下一首对结果集生效(同旧语义)。
### 17.5 内存对比(6000 首歌单场景)
| 项 | 改前(§14) | 改后(§17) |
|---|---|---|
| `ncm_song_t` | 1324B | 608B(-54%) |
| 歌曲对象数组 | 1000×1324B ≈ 1292KB(**且 1000 截断,6000 首只能看前 1000**) | 256×608B ≈ 152KB(任意长度恒定) |
| trackIds 索引 | 1000×8B = 8KB(截断) | 16384×8B = 128KB(全量) |
| 有效位图 | — | 256B |
| **合计(歌曲数据)** | **≈1300KB(有截断)** | **≈280KB(无截断)** |
| ncm bss 总量(`size`) | 2052068 | **1080900**(-972KB,-47%) |
6000 首全量物化的假想成本 6000×1324B≈7.9MB(14MB 预算下不可行)→ 滑窗后与歌单长度无关。
### 17.6 验证记录(0 真实 NCM 请求)
| 项 | 方式 | 结果 |
|---|---|---|
| 窗口基本语义 | `bench/pagination_sim.c`(已更新到新语义,离线 stub 驱动真实 appstate) | **全 PASS**:130 首整装入窗(1 detail + 2 song_detail);5191 首无截断(索引全量 5191、52 批网格、末首可达、出窗槽 copy 失败、越界无操作);空歌单 0 |
| 6000 首滚动全程 | `bench/window_sim.c`(新;真实 appstate + 真实 songmeta 盘缓存 /tmp) | **全 PASS**:首屏 1+1 请求;下滚 750 步(步进 8 行)每步视口 12 行全物化、song_detail 恰 60 次(无放大)、有效槽恒 ≤256;**回滚全程 +0 请求**(盘缓存全命中,6000 meta 文件落盘);窗口滑出播放曲后 `song_fetch` 不滑窗不崩、盘命中免请求、尾部/越界钳制正确 |
| core 回归 | `build_core.sh` + `test_ncm_core_host` + `qemu-mipsel out/test_ncm_core_mipsel` | 宿主 + qemu 全 PASS(结构体瘦身不影响解析断言) |
| mipsel 构建 | `bash build_ncm.sh`(自有代码 `-Wall -Wextra`) | **0 warning**;`text 865604 / data 1480 / bss 1080900` |
| 宿主离屏渲染 | `build_ncm_host.sh` + `MEOW_UI_DUMP`(mock,0 网络) | home/playlist/player/lands/settings/search/login 渲染正常:歌单 6 行 + footer `1-6 6`(视口区间格式)、黑胶 + 快照歌名/歌手 + 歌词高亮;自有代码 0 warning(仅 SDK `meow_ui_vol.c` 既有告警) |
### 17.7 md5(本批改动文件 + 产物)
```
b32e8a80c12de4dac2e4ad52d26ae55c ncm # mipsel 静态 App(新;未部署)
783d3ddf9bfc1905f732f688c54c5447 ncm_host # 宿主离屏调试二进制
321039c8743af6ccd934c63a3a161e84 app/ncm_app_internal.h
bde871b33298cf25187054650560754a core/ncm_api.h
3c4122e72cc701aa5d0d95862b0201d1 app/logic/appstate.c
dd8fbb70ba5e0978fa75501135c473a3 app/logic/songmeta.c
cb44e2385108ba6cdad242aecf635b64 app/ui/playlist.c
533dafebce47bd60355a1efc7719db9c app/ui/player.c
a5447ac5e6f2ba83405a7a3c19c88dc9 app/play/player_manager.c
74eded2b9a220aba9a50832ff9c6914c app/play/ipc_media.c
d2c2505eede96ddb1387209681894b83 app/ui/search.c
564630846354d3e202de303435e82c59 app/test/test_ncm_app.c
00a1445f83e98a068c4219593bb7d6d8 bench/pagination_sim.c
b2b6869990c8ef3906c100f18c7194e8 bench/window_sim.c
3159c53adf59967cacdad50a58946858 build_ncm.sh
430211ee6895c26c9871da22e9aede76 build_ncm_host.sh
```
### 17.8 出入清单(只改 `patch_v2/ncm_app/`)
```
修改:
core/ncm_api.h # ncm_song_t 瘦身 1324B→608B(id24/name128/art96/al96/pic256/dt int32)
app/ncm_app_internal.h # 删 NCM_SONG_MAX;+NCM_TRACKID_MAX 16384 / NCM_SONG_WINDOW 256 /
# song_valid[]/window_base;g_play.play_song 快照;窗口 API + songmeta 声明
app/logic/appstate.c # 滑动窗口(g_song_mu/滑窗 memmove/批网格预取/盘缓存补载)+
# song_copy/range_loaded/song_fetch/adopt_songs + 兼容入口改窗口语义 +
# autoplay 走 song_copy + logout 清窗口 + strncpy→ncm_strlcpy 消告警
app/ui/playlist.c # 触底分页退役 → 视口 ensure(pl_maybe_ensure)+ 占位行 + footer 视口区间
app/ui/player.c # 元数据读播放快照(cur_song_meta)+ 快照 id 变化纠正封面/歌名 +
# 占位行可点(播放线程物化)
app/play/player_manager.c # run_song 起播快照 + song_local_file 按 id + 边界 track_ids_count +
# 歌词 worker 读快照 + player_manager_current_song 访问器
app/play/ipc_media.c # META title/artist/album 读播放快照
app/ui/search.c # 结果经 appstate_adopt_songs 收编为窗口播放上下文
app/test/test_ncm_app.c # mock 补窗口字段(song_valid/track_ids_count)+ 播放快照
bench/pagination_sim.c # 更新到窗口语义(5191 无截断/出窗 copy 失败/网格批数)
build_ncm.sh / build_ncm_host.sh # +app/logic/songmeta.c
ncm / ncm_host # 重编产物
新增:
app/logic/songmeta.c # 歌曲元数据盘缓存(<cache_dir>/meta/<id>.bin,magic/ver/size 头)
bench/window_sim.c # 6000 首滚动全程离线验证(0 请求,盘缓存回滚 0 新增请求)
REPORT.md # 本批记录(§17)
(未动 sdk/、meow_launcherd/、meow_audiod/、vendor/;未部署真机;未动设备系统/恢复区)
```
### 17.9 诚实出入 / 已知限制
- **真机未部署/未验**(风控 + 设备音频线排查中;部署留主会话)。真机验收点:① 6000+ 歌单
全程下滚/回滚无占位残影、footer 视口区间随滚动更新;② 占位行直点能播(fetch 物化);
③ 播放中猛滑列表把当前曲滑出窗 → 播放/歌词/媒体卡片不断;④ 二次进入歌单离线可浏览
已拉过的部分(meta 盘缓存);⑤ 歌单 >16384 首时 trackIds 索引截断(footer 总数仍对,
尾部不可达——16384 是 128KB bss 的取舍,当前用户最大歌单 6000+ 有足够余量)。
- **单写者假设**:ensure/page 仅由 playlist worker 驱动(`g_plthr_on` 互斥),播放线程
`song_fetch` 不写窗口;搜索 worker 的 cloudsearch 直接写 `songs[]` 前半再 `adopt_songs`
收编(与旧代码同款的单写者时序,未加新锁)。
- **快照滞后一瞬**:切歌瞬间(next/prev/占位行直点)播放页歌名/封面仍是旧曲快照,
play 线程物化后(通常 <100ms,窗外需一次盘读/网络)由 tick 按快照 id 纠正——一帧级旧
内容,与 §14.7 歌词锁残余同级接受。
- **切歌单时播放上下文**:打开新歌单会重建索引/清窗口(同 §14 既有行为),正在播的旧歌单
曲目靠快照续播(比旧模型更安全:旧模型 memset 后 id/dur 全丢);但 cur_song 下标语义
随新歌单索引变化(既有竞态,未扩大)。
- **meta 盘缓存无版本化清理**:结构体再改版靠 ver/size 判 miss 自动重建;无 TTL
(歌曲改名/换封面不主动失效——与 ncm_cache 同级的「缓存可再生」取舍)。
- **host 构建有 SDK `meow_ui_vol.c`「system 返回值忽略」告警**(SDK 源,非本 App;mipsel musl 无)。
+247 -41
View File
@@ -7,6 +7,9 @@
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data" #define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"
/* 滑动窗口互斥(REPORT §17):窗口读写(滑窗/物化/拷贝)全走此锁,见下方窗口节注释。 */
static pthread_mutex_t g_song_mu = PTHREAD_MUTEX_INITIALIZER;
int appstate_init(void) int appstate_init(void)
{ {
const char *dd = getenv("MEOW_DATA_DIR"); const char *dd = getenv("MEOW_DATA_DIR");
@@ -16,7 +19,7 @@ int appstate_init(void)
ncm_ctx_init(&g_st.ctx, g_st.data_dir); /* device_id + cookie 载入 + cache */ ncm_ctx_init(&g_st.ctx, g_st.data_dir); /* device_id + cookie 载入 + cache */
if (g_st.ctx.cache.dir[0]) if (g_st.ctx.cache.dir[0])
strncpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir - 1); ncm_strlcpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir);
else else
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir); ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
@@ -80,7 +83,12 @@ void appstate_logout(void)
g_st.nickname[0] = 0; g_st.nickname[0] = 0;
g_st.active_playlist = -1; g_st.active_playlist = -1;
g_st.playing_song = -1; g_st.playing_song = -1;
pthread_mutex_lock(&g_song_mu);
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
g_st.window_base = 0;
g_st.song_count = 0; g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
meow_config_remove("ncm.uid"); meow_config_remove("ncm.uid");
meow_config_remove("ncm.nickname"); meow_config_remove("ncm.nickname");
cover_cache_reset(); cover_cache_reset();
@@ -120,6 +128,219 @@ int appstate_load_playlists(void)
return rc; return rc;
} }
/* ============================================================
* 滑动窗口(REPORT §17,三期第三批)
* songs[i] ↔ 全局下标 window_base+i;song_valid[] 为槽位有效位图。
* 写侧(滑窗/物化)与读侧(song_copy)全走 g_song_mu;网络/盘 IO 在锁外做,
* 锁内只做内存搬移,UI 行回调(song_copy)不被网络阻塞。
* 单写者假设:ensure/page 仅由 playlist worker(及兼容入口)驱动,同一时刻最多一个。
* ============================================================ */
static int slot_valid_locked(int gidx)
{
int s = gidx - g_st.window_base;
return s >= 0 && s < NCM_SONG_WINDOW && g_st.song_valid[s];
}
static void win_recount_locked(void)
{
int n = 0;
for (int i = 0; i < NCM_SONG_WINDOW; i++)
if (g_st.song_valid[i]) n++;
g_st.song_count = n;
}
/* 锁内滑窗:重叠区 songs+valid 一起 memmove 保留,非重叠端清零(出窗槽位失效)。
* 出窗歌曲的元数据此前已落盘(songmeta),丢弃无数据损失。 */
static void win_slide_locked(int new_base)
{
int old_base = g_st.window_base;
if (new_base == old_base) return;
int ov_lo = (old_base > new_base) ? old_base : new_base;
int ov_hi = (old_base + NCM_SONG_WINDOW < new_base + NCM_SONG_WINDOW)
? old_base + NCM_SONG_WINDOW : new_base + NCM_SONG_WINDOW;
if (ov_lo < ov_hi) {
int src = ov_lo - old_base, dst = ov_lo - new_base, n = ov_hi - ov_lo;
memmove(&g_st.songs[dst], &g_st.songs[src], (size_t)n * sizeof(ncm_song_t));
memmove(&g_st.song_valid[dst], &g_st.song_valid[src], (size_t)n);
if (dst > 0) memset(g_st.song_valid, 0, (size_t)dst);
if (dst + n < NCM_SONG_WINDOW)
memset(&g_st.song_valid[dst + n], 0, (size_t)(NCM_SONG_WINDOW - dst - n));
} else {
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
}
g_st.window_base = new_base;
win_recount_locked();
}
int appstate_range_loaded(int lo, int hi)
{
if (lo < 0) lo = 0;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
if (lo >= hi) return 1;
pthread_mutex_lock(&g_song_mu);
int ok = 1;
for (int i = lo; i < hi; i++)
if (!slot_valid_locked(i)) { ok = 0; break; }
pthread_mutex_unlock(&g_song_mu);
return ok;
}
int appstate_song_copy(int gidx, ncm_song_t *out)
{
if (!out) return -1;
pthread_mutex_lock(&g_song_mu);
int s = gidx - g_st.window_base;
int ok = (s >= 0 && s < NCM_SONG_WINDOW && g_st.song_valid[s]);
if (ok) *out = g_st.songs[s];
pthread_mutex_unlock(&g_song_mu);
return ok ? 0 : -1;
}
int appstate_song_ensure_range(int lo, int hi)
{
if (lo < 0) lo = 0;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
if (lo >= hi) return NCM_OK;
/* 目标基址:请求区间居中(前后留余量,滚动反向时不立即滑窗),钳制到合法范围。 */
int base;
if (hi - lo <= NCM_SONG_WINDOW) {
base = (lo + hi) / 2 - NCM_SONG_WINDOW / 2;
if (base > lo) base = lo;
if (base + NCM_SONG_WINDOW < hi) base = hi - NCM_SONG_WINDOW;
int maxb = g_st.track_ids_count - NCM_SONG_WINDOW;
if (maxb < 0) maxb = 0;
if (base > maxb) base = maxb;
if (base < 0) base = 0;
} else {
base = lo; /* 请求区间超窗(不应发生):覆盖前缀 */
}
pthread_mutex_lock(&g_song_mu);
int covered = 1;
for (int i = lo; i < hi; i++)
if (!slot_valid_locked(i)) { covered = 0; break; }
if (!covered && base != g_st.window_base) win_slide_locked(base);
else base = g_st.window_base; /* 已覆盖:维持原基址 */
pthread_mutex_unlock(&g_song_mu);
if (covered) return NCM_OK;
/* 逐段补齐 [lo,hi) 内 invalid 槽:先试盘缓存(songmeta),miss 攒批 ≤NCM_SONG_BATCH
* 走 song_detail;成功写窗 + 落盘。网络/盘 IO 均在锁外,锁内仅写槽。 */
static ncm_song_t s_fetch[NCM_SONG_BATCH]; /* 单写者(playlist worker),免 60KB 栈 */
int rc = NCM_OK;
for (int a = lo; a < hi && rc == NCM_OK; ) {
pthread_mutex_lock(&g_song_mu);
int va = slot_valid_locked(a);
pthread_mutex_unlock(&g_song_mu);
if (va) { a++; continue; }
int b = a + 1;
while (b < hi && b - a < NCM_SONG_BATCH) {
pthread_mutex_lock(&g_song_mu);
int vb = slot_valid_locked(b);
pthread_mutex_unlock(&g_song_mu);
if (vb) break;
b++;
}
/* 批网格对齐预取:把 [a,b) 扩到 song_detail 批边界(§17.4——逐行滚动时若按原始
* 小区间拉取,song_detail 调用次数会放大到 ~歌单长度/滚动步长;对齐到 100 首
* 网格后每次补齐整批,总请求数 ≈ 歌单长度/NCM_SONG_BATCH)。 */
int a0 = (a / NCM_SONG_BATCH) * NCM_SONG_BATCH;
int b0 = ((b + NCM_SONG_BATCH - 1) / NCM_SONG_BATCH) * NCM_SONG_BATCH;
int wend2 = g_st.window_base + NCM_SONG_WINDOW;
if (wend2 > g_st.track_ids_count) wend2 = g_st.track_ids_count;
if (a0 < g_st.window_base) a0 = g_st.window_base;
if (b0 > wend2) b0 = wend2;
if (b0 - a0 > NCM_SONG_BATCH) b0 = a0 + NCM_SONG_BATCH;
/* 本段 [a0,b0):盘命中直接写窗,有效槽跳过,miss 记录待批拉 */
int64_t ids[NCM_SONG_BATCH];
int gslot[NCM_SONG_BATCH];
int m = 0;
for (int i = a0; i < b0; i++) {
pthread_mutex_lock(&g_song_mu);
int vv = slot_valid_locked(i);
pthread_mutex_unlock(&g_song_mu);
if (vv) continue;
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)g_st.song_ids[i]);
ncm_song_t ms;
if (songmeta_load(g_st.cache_dir, sid, &ms) == NCM_OK) {
pthread_mutex_lock(&g_song_mu);
int s = i - g_st.window_base;
if (s >= 0 && s < NCM_SONG_WINDOW) {
g_st.songs[s] = ms;
g_st.song_valid[s] = 1;
}
pthread_mutex_unlock(&g_song_mu);
} else {
ids[m] = g_st.song_ids[i];
gslot[m] = i;
m++;
}
}
if (m > 0) {
int bn = 0;
int r2 = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie,
ids, m, s_fetch, NCM_SONG_BATCH, &bn);
if (r2 != NCM_OK) { rc = r2; break; }
if (bn > m) bn = m;
for (int k = 0; k < bn; k++) { /* song_detail 结果与请求 ids 同序 */
pthread_mutex_lock(&g_song_mu);
int s = gslot[k] - g_st.window_base;
if (s >= 0 && s < NCM_SONG_WINDOW) {
g_st.songs[s] = s_fetch[k];
g_st.song_valid[s] = 1;
}
pthread_mutex_unlock(&g_song_mu);
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)ids[k]);
songmeta_save(g_st.cache_dir, sid, &s_fetch[k]);
}
if (bn < m) rc = NCM_ERR_JSON; /* 部分返回:缺槽留 invalid,下次 ensure 再试 */
}
a = b;
}
pthread_mutex_lock(&g_song_mu);
win_recount_locked();
pthread_mutex_unlock(&g_song_mu);
return rc;
}
int appstate_song_fetch(int gidx, ncm_song_t *out)
{
if (!out) return NCM_ERR_ARG;
if (appstate_song_copy(gidx, out) == 0) return NCM_OK; /* 窗内直接命中 */
if (gidx < 0 || gidx >= g_st.track_ids_count) return NCM_ERR_ARG;
int64_t id = g_st.song_ids[gidx];
if (id == 0) return NCM_ERR_ARG; /* 无索引(如搜索上下文窗外) */
char sid[24];
snprintf(sid, sizeof sid, "%lld", (long long)id);
if (songmeta_load(g_st.cache_dir, sid, out) == NCM_OK) return NCM_OK; /* 盘缓存 */
int bn = 0;
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &id, 1, out, 1, &bn);
if (rc != NCM_OK) return rc;
if (bn < 1) return NCM_ERR_NOTFOUND;
songmeta_save(g_st.cache_dir, sid, out);
return NCM_OK;
}
void appstate_adopt_songs(int n)
{
pthread_mutex_lock(&g_song_mu);
if (n < 0) n = 0;
if (n > NCM_SONG_WINDOW) n = NCM_SONG_WINDOW;
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
for (int i = 0; i < n; i++) {
g_st.song_valid[i] = 1;
g_st.song_ids[i] = strtoll(g_st.songs[i].id, NULL, 10);
}
g_st.window_base = 0;
g_st.song_count = n;
g_st.track_ids_count = n;
pthread_mutex_unlock(&g_song_mu);
}
int appstate_load_playlist_songs_page(int idx, int offset, int limit) int appstate_load_playlist_songs_page(int idx, int offset, int limit)
{ {
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG; if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
@@ -130,61 +351,44 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit)
if (offset < 0) offset = 0; if (offset < 0) offset = 0;
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。 /* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
* 回到 §10.11 已验证路径并做**真分页**:detail(n=1)取【全量】trackIds 入 song_ids[], * §14 的分页(append-only 到 NCM_SONG_MAX=1000 截断)在 §17 升级为滑动窗口:
* song_detail 按 ≤NCM_SONG_BATCH 分批拉歌曲对象(含 al.picUrl),offset 为「下一页」起始; * detail(n=1)仍取【全量】trackIds 入 song_ids[](≤ NCM_TRACKID_MAX,支撑 6000+ 首),
* 列表触底驱动 offset 前进,song_count 逐页累加(不再一次性拉满,大歌单不冻 UI, * 歌曲对象只物化视口附近的 NCM_SONG_WINDOW 首(offset 为「要确保的区间」起点)。 */
* 也不把「第 N 页之后的歌」挡在 256 之外——NCM_SONG_MAX 上限提升到 1000,见 §14)。 */
if (offset == 0) { if (offset == 0) {
/* 重头:重建 trackIds 索引 + 清空已物化歌曲(song_count 无论成败都归零)。 */ /* 重头:清窗口 + 重建 trackIds 索引(song_count/track_ids_count 无论成败都归零)。 */
pthread_mutex_lock(&g_song_mu);
memset(g_st.song_valid, 0, sizeof g_st.song_valid);
memset(g_st.song_ids, 0, sizeof g_st.song_ids); memset(g_st.song_ids, 0, sizeof g_st.song_ids);
memset(g_st.songs, 0, sizeof g_st.songs); g_st.window_base = 0;
g_st.song_count = 0; g_st.song_count = 0;
g_st.track_ids_count = 0; g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
int idn = 0; int idn = 0;
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id, int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
NULL, 0, NULL, 0, NULL, 0, NULL, 0,
g_st.song_ids, NCM_SONG_MAX, &idn); g_st.song_ids, NCM_TRACKID_MAX, &idn);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
g_st.track_ids_count = idn; g_st.track_ids_count = idn;
/* 首屏只需第 0 页:song_detail 只拉前 limit 首,其余待滚动触底再拉(分页驱动)。 */ appstate_apply_playlist_cover(idx);
} else if (g_st.track_ids_count == 0) { } else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */ /* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG; return NCM_ERR_ARG;
} }
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部/上限 */ if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部 */
if (offset >= NCM_SONG_MAX) return NCM_OK; /* 内存上限:不再物化更多 */ int hi = offset + limit;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
int can = g_st.track_ids_count - offset; return appstate_song_ensure_range(offset, hi);
int room = NCM_SONG_MAX - offset;
if (can > limit) can = limit; /* 每页 ≤ limit(保守 100) */
if (can > room) can = room; /* 不越界 song_ids[]/songs[] */
int bn = 0;
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[offset],
can, &g_st.songs[offset], NCM_SONG_MAX - offset, &bn);
if (rc != NCM_OK) return rc; /* 失败:song_count 不变(保留已载页) */
g_st.song_count = offset + bn; /* 成功:累加本页 */
appstate_apply_playlist_cover(idx);
return NCM_OK;
} }
int appstate_load_playlist_songs(int idx) int appstate_load_playlist_songs(int idx)
{ {
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */ /* 兼容入口:载索引 + 物化首个窗口。 */
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH); int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc; if (rc != NCM_OK) return rc;
int off = g_st.song_count; int hi = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) { return appstate_song_ensure_range(0, hi);
rc = appstate_load_playlist_songs_page(idx, off, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int nxt = g_st.song_count;
if (nxt <= off) break; /* 无进展:防死循环 */
off = nxt;
}
return NCM_OK;
} }
void appstate_apply_playlist_cover(int idx) void appstate_apply_playlist_cover(int idx)
@@ -203,7 +407,7 @@ void appstate_set_playing_song(int idx, const char *cover_url)
int appstate_current_playing(void) int appstate_current_playing(void)
{ {
if (g_st.playing_song < 0 || g_st.playing_song >= g_st.song_count) return -1; if (g_st.playing_song < 0 || g_st.playing_song >= g_st.track_ids_count) return -1;
return g_st.playing_song; return g_st.playing_song;
} }
@@ -227,18 +431,20 @@ static int autoplay_try_playlist(int pl)
return 0; return 0;
} }
char cpath[600]; char cpath[600];
for (int i = 0; i < g_st.song_count; i++) { for (int i = 0; i < g_st.track_ids_count; i++) { /* 滑窗模型:锁内拷贝窗内有效槽 */
if (ncm_cache_lookup(&g_st.ctx.cache, g_st.songs[i].id, ".mp3", ncm_song_t s;
if (appstate_song_copy(i, &s) != 0) break; /* 出窗即停(首页之后未物化) */
if (ncm_cache_lookup(&g_st.ctx.cache, s.id, ".mp3",
cpath, sizeof cpath) == NCM_OK) { cpath, sizeof cpath) == NCM_OK) {
g_st.active_playlist = pl; g_st.active_playlist = pl;
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play", meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play",
pl, i, g_st.songs[i].id); pl, i, s.id);
player_manager_play(i); player_manager_play(i);
return 1; return 1;
} }
} }
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first page %d songs, no cached track", meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first window %d/%d songs, no cached track",
pl, g_st.song_count); pl, g_st.song_count, g_st.track_ids_count);
return 0; return 0;
} }
+68
View File
@@ -0,0 +1,68 @@
/* logic/songmeta.c — 歌曲元数据本地缓存(REPORT §17 滑动窗口配套)。
*
* 拉取成功的 song_detail 结果逐首序列化到 <cache_dir>/meta/<id>.bin;滑动窗口补载时
* 先读盘(命中免 song_detail 请求),离线/二次进入歌单滚动全程零网络。
*
* 取舍(刻意从简):
* - 不进 ncm_cache LRU 池:meta 每首 ≈600B,一万首 ≈6MB,不参与 10GB 级 LRU 淘汰,
* 也不被 ncm_cache_clear(只删 song_* 顶层文件)误清——清音频缓存不丢浏览元数据。
* - 格式 = 头(magic/ver/size) + ncm_song_t 原始内存镜像:同机同构读写,跨架构/结构体
* 改版由 ver+size 失配判 miss(缓存可再生,最坏结果 = 回退 song_detail 网络拉取)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#define SONGMETA_MAGIC 0x4D4D434Eu /* "NCMM" */
#define SONGMETA_VER 1
typedef struct {
uint32_t magic;
uint16_t ver;
uint16_t size; /* sizeof(ncm_song_t),失配判 miss */
} songmeta_hdr_t;
static int songmeta_path(const char *dir, const char *id, char *out, size_t sz)
{
if (!dir || !dir[0] || !id || !id[0]) return -1;
int n = snprintf(out, sz, "%s/meta/%s.bin", dir, id);
return (n > 0 && (size_t)n < sz) ? 0 : -1;
}
int songmeta_load(const char *cache_dir, const char *song_id, ncm_song_t *out)
{
char path[NCM_CACHE_DIR_MAX + 48];
if (songmeta_path(cache_dir, song_id, path, sizeof path) != 0) return NCM_ERR_NOENT;
FILE *f = fopen(path, "rb");
if (!f) return NCM_ERR_NOENT;
songmeta_hdr_t h;
int ok = (fread(&h, 1, sizeof h, f) == sizeof h) &&
h.magic == SONGMETA_MAGIC && h.ver == SONGMETA_VER &&
h.size == sizeof(ncm_song_t) &&
(fread(out, 1, sizeof(ncm_song_t), f) == sizeof(ncm_song_t));
fclose(f);
return ok ? NCM_OK : NCM_ERR_NOENT;
}
int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t *s)
{
if (!cache_dir || !cache_dir[0] || !song_id || !song_id[0] || !s) return NCM_ERR_ARG;
char dir[NCM_CACHE_DIR_MAX + 16];
int n = snprintf(dir, sizeof dir, "%s/meta", cache_dir);
if (n <= 0 || (size_t)n >= sizeof dir) return NCM_ERR_ARG;
mkdir(cache_dir, 0755); /* 幂等(缓存目录通常已由 ncm_cache_init 建好) */
mkdir(dir, 0755);
char path[NCM_CACHE_DIR_MAX + 48];
if (songmeta_path(cache_dir, song_id, path, sizeof path) != 0) return NCM_ERR_ARG;
FILE *f = fopen(path, "wb");
if (!f) return NCM_ERR_IO;
songmeta_hdr_t h = { SONGMETA_MAGIC, SONGMETA_VER, (uint16_t)sizeof(ncm_song_t) };
int ok = (fwrite(&h, 1, sizeof h, f) == sizeof h) &&
(fwrite(s, 1, sizeof(ncm_song_t), f) == sizeof(ncm_song_t));
fclose(f);
if (!ok) remove(path); /* 半截文件不留(下次判 miss 重拉) */
return ok ? NCM_OK : NCM_ERR_IO;
}
+49 -20
View File
@@ -41,15 +41,17 @@
/* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */ /* ---- 数据规模(内存纪律:歌单/歌曲大列表虚拟化显示,数组有界;硬约束 R3P2 ~14MB)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */ #define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
/* 单个歌单曲目上限 = 分页驱动下的「单页渲染上限」(REPORT §14,三期第一批): /* 滑动窗口(REPORT §17,三期第三批,取代 §14 的 NCM_SONG_MAX=1000 硬截断):
* detail(/v6/playlist/detail, n=1)取回全量 trackIds 入 song_ids[](≤ NCM_SONG_MAX); * song_ids[NCM_TRACKID_MAX] = detail(/v6/playlist/detail, n=1)取回的全量 trackIds
* song_detail 按 NCM_SONG_BATCH 分批拉歌词对象,列表触底自动加载下一页,直到 * (int64×16384=128KB bss,支撑任意长度歌单,不再 1000 截断);
* song_count == track_ids_count(歌单尾部)或 == NCM_SONG_MAX(内存上限,footer 显示 x/总数)。 * songs[NCM_SONG_WINDOW] = 物化滑动窗口(256 首 × ~608B ≈ 152KB bss,与歌单长度无关);
* 不再硬截断 256(NCM_SONG_MAX 从 256 提升到 1000,约 1.32KB/首 → 歌曲数组 ~1.37MB,14MB 内安全)。 * song_valid[NCM_SONG_WINDOW]= 槽位有效位图(songs[i] ↔ 全局下标 window_base+i);
* 数值权衡见 REPORT §14.8(可再上调,代价是 bss 线性增长:每 +100 首 ≈ +132KB)。 */ * 滚动出窗的槽位失效(数据已序列化到 <cache_dir>/meta/<id>.bin),入窗经 song_detail 按需
#define NCM_SONG_MAX 1000 /* 歌单曲目/歌曲对象上限(append-only 分页加载) */ * 拉取(盘缓存命中免请求)。列表 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; #define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
* 同时作「下一页」加载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB */ * 窗口补载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB) */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */ #define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */ #define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */ #define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
@@ -84,6 +86,10 @@ typedef struct ncm_play_run {
int err; /* 最近一次播放错误(0=无) */ int err; /* 最近一次播放错误(0=无) */
char errmsg[96]; char errmsg[96];
int has_track; /* 当前是否有可播曲目(含 url 获取中) */ 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; } ncm_play_run_t;
/* ---- 音质档位字符串(设置页 + 持久化用)---- */ /* ---- 音质档位字符串(设置页 + 持久化用)---- */
@@ -113,14 +119,19 @@ typedef struct ncm_app_state {
int active_playlist; /* 当前打开歌单的下标;-1=无 */ int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。 /* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_SONG_MAX,分页索引); * 滑动窗口模型(REPORT §17):
* songs[] = 已物化的歌曲对象(append-only,与 song_ids[] 前 song_count 首一一对应); * song_ids[] = detail 返回的全量 trackIds(≤ NCM_TRACKID_MAX,全局下标索引);
* song_count = 已物化数(列表 count = song_count);track_ids_count = trackIds 总数(≥ song_count, * songs[] = 物化滑动窗口,songs[i] ↔ 全局下标 window_base+i;
* 其差值 = 仍有「下一页」待 song_detail 分批拉取)。 */ * song_valid[] = 槽位有效位图(出窗=失效,入窗=物化后置位);
int64_t song_ids[NCM_SONG_MAX]; * window_base = 窗口起始全局下标;song_count = 窗口内有效槽位数(≤ NCM_SONG_WINDOW);
ncm_song_t songs[NCM_SONG_MAX]; * 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 song_count;
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_SONG_MAX) */ int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_TRACKID_MAX) */
/* 播放页视图态 */ /* 播放页视图态 */
int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */ int playing_song; /* 正在播放歌曲在 songs[] 下标(UI 侧缓存);-1=无 */
@@ -258,12 +269,24 @@ void appstate_set_logged_in(const char *uid, const char *nickname);
void appstate_logout(void); void appstate_logout(void);
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */ int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
int appstate_load_playlists(void); /* 拉我的歌单 */ int appstate_load_playlists(void); /* 拉我的歌单 */
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */ int appstate_load_playlist_songs(int idx); /* 兼容:载索引 + 首个窗口(内部走 ensure) */
int appstate_load_playlist_songs_page(int idx, int offset, int limit); int appstate_load_playlist_songs_page(int idx, int offset, int limit);
/* detail(全量 trackIds)后按页拉 song_detail: /* 兼容入口(滑动窗口语义,REPORT §17):
* offset=0 重头(重建 song_ids[]/track_ids_count,清空后载第 0 页); * offset=0 重头(detail 全量 trackIds 重建索引、清窗口、
* offset>0 追加第 offset 页到 g_st.songs[offset..]; * 物化 [0,limit));offset>0 = 物化 [offset,offset+limit)
* 写 g_st.song_count(=offset+本页实际数)。 */ * (滑窗+盘缓存+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,成功落盘。
* 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_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url); void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */ int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
@@ -311,6 +334,12 @@ int64_t player_manager_pos_ms(void);
int64_t player_manager_dur_ms(void); int64_t player_manager_dur_ms(void);
int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */ int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */
int player_manager_error(void); int player_manager_error(void);
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
/* 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:媒体会话上报/收指令 */ /* ipc_media:媒体会话上报/收指令 */
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */ int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
+5 -4
View File
@@ -108,10 +108,11 @@ static void send_meta(int fd, uint32_t req_id)
static char title[128], artist[128], album[128]; static char title[128], artist[128], album[128];
title[0] = artist[0] = album[0] = 0; title[0] = artist[0] = album[0] = 0;
if (idx >= 0 && idx < g_st.song_count) { ncm_song_t sm;
snprintf(title, sizeof title, "%s", g_st.songs[idx].name); if (idx >= 0 && player_manager_current_song(&sm) == 0) { /* 播放快照(窗口无关,§17) */
snprintf(artist, sizeof artist, "%s", g_st.songs[idx].art_name); snprintf(title, sizeof title, "%s", sm.name);
snprintf(album, sizeof album, "%s", g_st.songs[idx].album_name); snprintf(artist, sizeof artist, "%s", sm.art_name);
snprintf(album, sizeof album, "%s", sm.album_name);
} }
snprintf(mm.title, sizeof mm.title, "%s", title); snprintf(mm.title, sizeof mm.title, "%s", title);
snprintf(mm.artist, sizeof mm.artist, "%s", artist); snprintf(mm.artist, sizeof mm.artist, "%s", artist);
+45 -21
View File
@@ -41,6 +41,18 @@ int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu);
int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; } int64_t player_manager_dur_ms(void) { int64_t d; pthread_mutex_lock(&g_play.mu); d = g_play.dur_ms; pthread_mutex_unlock(&g_play.mu); return d; }
int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; } int player_manager_error(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.err; pthread_mutex_unlock(&g_play.mu); return e; }
/* 播放曲快照(REPORT §17):run_song 起播时物化并拷入 g_play.play_song(mu 保护),
* 之后播放/歌词/IPC/UI 一律读快照——窗口滑出当前播放曲不影响播放,不许崩。 */
int player_manager_current_song(ncm_song_t *out)
{
if (!out) return -1;
pthread_mutex_lock(&g_play.mu);
int ok = g_play.play_song_valid;
if (ok) *out = g_play.play_song;
pthread_mutex_unlock(&g_play.mu);
return ok ? 0 : -1;
}
void player_manager_set_quality(int level, int cache_gb) void player_manager_set_quality(int level, int cache_gb)
{ {
g_level = level; g_level = level;
@@ -83,7 +95,7 @@ void player_manager_next(int dir)
pthread_mutex_lock(&g_play.mu); pthread_mutex_lock(&g_play.mu);
int c = g_play.cur_song + dir; int c = g_play.cur_song + dir;
if (c < 0) c = 0; if (c < 0) c = 0;
if (c >= g_st.song_count) c = g_st.song_count - 1; if (c >= g_st.track_ids_count) c = g_st.track_ids_count - 1; /* 滑窗:边界=全量索引数 */
if (c != g_play.cur_song) { if (c != g_play.cur_song) {
g_play.cur_song = c; g_play.cur_song = c;
g_play.cmd = 0; g_play.cmd = 0;
@@ -100,6 +112,7 @@ void player_manager_stop(void)
g_play.cur_song = -1; g_play.cur_song = -1;
g_play.state = NCM_PC_STOPPED; g_play.state = NCM_PC_STOPPED;
g_play.pos_ms = 0; g_play.pos_ms = 0;
g_play.play_song_valid = 0; /* 快照失效 */
g_play.seq++; g_play.seq++;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
} }
@@ -111,12 +124,9 @@ static int download_cb(void *ctx, const unsigned char *data, size_t len)
} }
/* 得到本地 mp3 路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。 /* 得到本地 mp3 路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
* 返回 NCM_OK 或负错误码。 */ * 返回 NCM_OK 或负错误码。id 由调用方(run_song)从播放曲快照取,与窗口解耦(§17)。 */
static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes) static int song_local_file(const char *id, char *path, size_t psz, int64_t *bytes)
{ {
const ncm_song_t *s = &g_st.songs[idx];
const char *id = s->id;
/* 缓存命中 → 离线直接播 */ /* 缓存命中 → 离线直接播 */
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) == NCM_OK) { if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) == NCM_OK) {
struct stat st; struct stat st;
@@ -176,9 +186,21 @@ static void set_err(int err, const char *msg)
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */ /* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
static void run_song(int idx) static void run_song(int idx)
{ {
/* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进
* g_play.play_song——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */
ncm_song_t s;
if (appstate_song_copy(idx, &s) != 0) {
int fr = appstate_song_fetch(idx, &s);
if (fr != NCM_OK) { set_err(fr, "load failed"); return; }
}
pthread_mutex_lock(&g_play.mu);
g_play.play_song = s;
g_play.play_song_valid = 1;
pthread_mutex_unlock(&g_play.mu);
int64_t file_bytes = 0; int64_t file_bytes = 0;
char path[512]; char path[512];
int rc = song_local_file(idx, path, sizeof path, &file_bytes); int rc = song_local_file(s.id, path, sizeof path, &file_bytes);
if (rc != NCM_OK) { if (rc != NCM_OK) {
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)" const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
: (rc == NCM_ERR_AUTH) ? "session expired" : "load failed"; : (rc == NCM_ERR_AUTH) ? "session expired" : "load failed";
@@ -188,8 +210,7 @@ static void run_song(int idx)
ncm_strlcpy(g_path, path, sizeof g_path); ncm_strlcpy(g_path, path, sizeof g_path);
g_file_bytes = file_bytes; g_file_bytes = file_bytes;
const ncm_song_t *s = &g_st.songs[idx]; int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
int64_t dur = s->duration_ms > 0 ? s->duration_ms : 0;
mp3dec_ex_t dec; mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec); memset(&dec, 0, sizeof dec);
@@ -205,7 +226,7 @@ static void run_song(int idx)
g_play.dur_ms = dur; g_play.dur_ms = dur;
g_play.err = 0; g_play.err = 0;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s dur=%lldms", idx, s->id, (long long)dur); meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s dur=%lldms", idx, s.id, (long long)dur);
mp3d_sample_t pcm[PCM_CHUNK]; mp3d_sample_t pcm[PCM_CHUNK];
int64_t frames_done = 0; /* 已输出帧(每通道) */ int64_t frames_done = 0; /* 已输出帧(每通道) */
@@ -291,7 +312,7 @@ static void *player_thread(void *arg)
int st = g_play.state; int st = g_play.state;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
if (cur < 0 || cur >= g_st.song_count || st == NCM_PC_STOPPED) { if (cur < 0 || cur >= g_st.track_ids_count || st == NCM_PC_STOPPED) {
usleep(60 * 1000); usleep(60 * 1000);
continue; continue;
} }
@@ -303,9 +324,9 @@ static void *player_thread(void *arg)
int st2 = g_play.state; int st2 = g_play.state;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */ /* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
if (cur2 >= 0 && cur2 < g_st.song_count && st2 != NCM_PC_STOPPED && cur2 == cur) { if (cur2 >= 0 && cur2 < g_st.track_ids_count && st2 != NCM_PC_STOPPED && cur2 == cur) {
int nx = cur + 1; int nx = cur + 1;
if (nx < g_st.song_count) { player_manager_next(1); } if (nx < g_st.track_ids_count) { player_manager_next(1); }
else { player_manager_stop(); } else { player_manager_stop(); }
} else { } else {
usleep(50 * 1000); usleep(50 * 1000);
@@ -316,23 +337,27 @@ static void *player_thread(void *arg)
/* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延 /* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延
* 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ---- * 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ----
* 现存 race(§10.9 FM4,UI 读 lyrics 与 worker 写)为良性:数组定长、越界已防护, * §17:曲目 id 改读播放快照(g_play.play_song,mu 保护),不再依赖窗口内
* 最坏显示一帧旧/空行,下一 tick 自愈;与 g_st.songs 写读竞态同接受。 */ * g_st.songs[]——窗口滑出当前播放曲歌词照常。 */
static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_lyr_thr; static pthread_t g_lyr_thr;
static int g_lyr_thr_on; static int g_lyr_thr_on;
static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */ static volatile int g_lyr_state; /* 0=空闲/未拉 1=加载中 2=已拉完 */
static char g_lyr_song_id[32]; /* 歌词归属歌曲 id */ static char g_lyr_song_id[24]; /* 歌词归属歌曲 id */
static void *lyric_worker(void *arg) static void *lyric_worker(void *arg)
{ {
(void)arg; (void)arg;
char last_id[32] = ""; char last_id[24] = "";
while (!g_app_quit) { while (!g_app_quit) {
usleep(300 * 1000); usleep(300 * 1000);
int idx; char id[24];
pthread_mutex_lock(&g_play.mu); idx = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); pthread_mutex_lock(&g_play.mu);
if (idx < 0 || idx >= g_st.song_count) { int idx = g_play.cur_song;
int have = g_play.play_song_valid;
ncm_strlcpy(id, have ? g_play.play_song.id : "", sizeof id);
pthread_mutex_unlock(&g_play.mu);
if (idx < 0 || idx >= g_st.track_ids_count || !have) {
lyric_clear(); lyric_clear();
pthread_mutex_lock(&g_lyrmu); pthread_mutex_lock(&g_lyrmu);
g_lyr_song_id[0] = 0; g_lyr_song_id[0] = 0;
@@ -341,7 +366,6 @@ static void *lyric_worker(void *arg)
last_id[0] = 0; last_id[0] = 0;
continue; continue;
} }
const char *id = g_st.songs[idx].id;
if (!id[0]) continue; if (!id[0]) continue;
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */ if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
+10 -1
View File
@@ -42,7 +42,9 @@ static void mock_songs(void)
"Song Four", "Song Five", "Song Six" }; "Song Four", "Song Five", "Song Six" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C", static const char *ar[] = { "Artist A", "Artist B", "Artist C",
"Artist D", "Artist E", "Artist F" }; "Artist D", "Artist E", "Artist F" };
g_st.song_count = 6; g_st.song_count = 6; /* 滑窗模型:窗口 base=0,6 槽全有效(§17) */
g_st.window_base = 0;
g_st.track_ids_count = 6;
g_st.active_playlist = 0; g_st.active_playlist = 0;
for (int i = 0; i < 6; i++) { for (int i = 0; i < 6; i++) {
snprintf(g_st.songs[i].id, sizeof g_st.songs[i].id, "%ld", 1000L + i); snprintf(g_st.songs[i].id, sizeof g_st.songs[i].id, "%ld", 1000L + i);
@@ -51,6 +53,8 @@ static void mock_songs(void)
snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "Album %d", i + 1); snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "Album %d", i + 1);
g_st.songs[i].duration_ms = 180000 + i * 20000; g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = (i == 3) ? 1 : 0; g_st.songs[i].fee = (i == 3) ? 1 : 0;
g_st.song_valid[i] = 1;
g_st.song_ids[i] = 1000 + i;
} }
ncm_strlcpy(g_st.cover_url, "http://p1.music.126.net/cover0.jpg", sizeof g_st.cover_url); ncm_strlcpy(g_st.cover_url, "http://p1.music.126.net/cover0.jpg", sizeof g_st.cover_url);
g_st.playing_song = 0; g_st.playing_song = 0;
@@ -139,6 +143,8 @@ int main(int argc, char **argv)
g_play.cur_song = 0; g_play.cur_song = 0;
g_play.pos_ms = 94000; g_play.pos_ms = 94000;
g_play.dur_ms = 180000; g_play.dur_ms = 180000;
g_play.play_song = g_st.songs[0]; /* 播放快照(§17) */
g_play.play_song_valid = 1;
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
} else if (strcmp(name, "settings") == 0) { } else if (strcmp(name, "settings") == 0) {
g_st.logged_in = 1; g_st.logged_in = 1;
@@ -162,8 +168,11 @@ int main(int argc, char **argv)
snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "%s", al[i]); snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "%s", al[i]);
g_st.songs[i].duration_ms = 180000 + i * 20000; g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = 0; g_st.songs[i].fee = 0;
g_st.song_valid[i] = 1;
g_st.song_ids[i] = 1000 + i;
} }
g_st.song_count = 3; g_st.song_count = 3;
g_st.track_ids_count = 3;
} else { } else {
mock_playlists(); mock_playlists();
} }
+31 -16
View File
@@ -25,6 +25,7 @@ static ncm_rect_t r_cont;
static int g_custom_disc, g_custom_dyn; static int g_custom_disc, g_custom_dyn;
static int g_landscape; static int g_landscape;
static int g_last_pos_ms, g_last_state, g_last_line, g_last_song; 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 填充;各带绘制物/命中共用)。 */ /* 运行期几何(player_compute_geom 填充;各带绘制物/命中共用)。 */
static ncm_rect_t g_disc_r, g_dyn_r; /* 盘带 / 动态带 bbox(不重叠) */ static ncm_rect_t g_disc_r, g_dyn_r; /* 盘带 / 动态带 bbox(不重叠) */
@@ -40,7 +41,16 @@ static ncm_rect_t g_lyr; /* 歌词区 */
static int cur_song(void) static int cur_song(void)
{ {
int c = player_manager_playing_song(); int c = player_manager_playing_song();
return (c >= 0 && c < g_st.song_count) ? c : -1; 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) static void fmt_ms(int64_t ms, char *out, size_t sz)
@@ -188,10 +198,10 @@ static void plr_draw_dyn(void *ctx)
if (meow_ui_activity_active() != ACT_PLAYER) return; if (meow_ui_activity_active() != ACT_PLAYER) return;
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
const ncm_pal_t *pa = ncm_palette(g_dark); const ncm_pal_t *pa = ncm_palette(g_dark);
int sc = cur_song(); ncm_song_t sm;
const ncm_song_t *s = (sc >= 0) ? &g_st.songs[sc] : NULL; int have = (cur_song_meta(&sm) == 0);
const char *nm = s ? s->name : "暂无播放"; const char *nm = have ? sm.name : "暂无播放";
const char *ar = s ? s->art_name : ""; const char *ar = have ? sm.art_name : "";
if (g_landscape) { 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_title_y, ncm_text_fit(nm, g_info_w, 28), 28, th->onSurface);
@@ -204,7 +214,7 @@ static void plr_draw_dyn(void *ctx)
/* 进度条 + 时间 */ /* 进度条 + 时间 */
int pos = (int)player_manager_pos_ms(); int pos = (int)player_manager_pos_ms();
int dur = (int)player_manager_dur_ms(); int dur = (int)player_manager_dur_ms();
if (dur <= 0 && sc >= 0) dur = (int)(g_st.songs[sc].duration_ms); if (dur <= 0 && have) dur = (int)sm.duration_ms;
int frac = dur > 0 ? (int)((int64_t)pos * 1000 / dur) : 0; int frac = dur > 0 ? (int)((int64_t)pos * 1000 / dur) : 0;
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000; if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
char lp[16], rp[16]; char lp[16], rp[16];
@@ -302,11 +312,12 @@ void player_on_tap(int x, int y)
void player_play_song(int idx) void player_play_song(int idx)
{ {
if (idx < 0 || idx >= g_st.song_count) return; if (idx < 0 || idx >= g_st.track_ids_count) return;
/* 二期:黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅), /* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
* 无 al.picUrl(如无版权占位)时回落歌单封面。 */ * 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。 */
const ncm_song_t *s = &g_st.songs[idx]; ncm_song_t s;
const char *cover = (s && s->al_pic[0]) ? s->al_pic : g_st.cover_url; const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0])
? s.al_pic : g_st.cover_url;
player_manager_play(idx); player_manager_play(idx);
appstate_set_playing_song(idx, cover); appstate_set_playing_song(idx, cover);
cover_request(cover); cover_request(cover);
@@ -338,13 +349,16 @@ void player_tick(uint64_t now)
if (sc != g_last_song) { if (sc != g_last_song) {
g_last_song = sc; g_last_song = sc;
/* 切歌(next/prev/自动顺延)→ 请求新曲专辑封面(黑胶盘换图) */
if (sc >= 0) {
const ncm_song_t *s2 = &g_st.songs[sc];
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); /* 歌名/歌手变 */ 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()) { if (cover_changed()) {
cover_ack(); cover_ack();
meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h); meow_ui_invalidate(g_disc_r.x, g_disc_r.y, g_disc_r.w, g_disc_r.h);
@@ -376,6 +390,7 @@ static void plr_page_create(meow_view *v, void *ctx)
g_custom_dyn = meow_draw_custom_register(11, g_dyn_r.x, g_dyn_r.y, g_dyn_r.w, g_dyn_r.h, 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); plr_draw_dyn, NULL);
g_last_pos_ms = -1; g_last_state = -1; g_last_line = -1; g_last_song = -1; 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) static void plr_page_enter(meow_view *v, void *ctx)
+74 -61
View File
@@ -25,7 +25,9 @@ static char g_title[256];
static volatile int g_load_err; static volatile int g_load_err;
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y; static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
/* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。 */ /* 歌单详情加载后台 worker(大歌单可达数十秒,不能占 UI 线程)。
* §17 滑动窗口:worker 两种任务——①首屏(g_ens_first=1):重建 trackIds 索引 + 物化首段;
* ②滑窗补载:物化视口区间 [g_ens_lo,g_ens_hi)(ensure_range,盘缓存优先)。 */
static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_plmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_plthr; static pthread_t g_plthr;
static int g_plthr_on; static int g_plthr_on;
@@ -33,8 +35,9 @@ static volatile int g_plgen;
static int g_placked; static int g_placked;
static volatile int g_plloading; static volatile int g_plloading;
static volatile int g_plloaded_idx; static volatile int g_plloaded_idx;
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */ static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
static volatile int g_pl_more; /* 1=尚有下一页可加载(song_count < track_ids_count 且未到 NCM_SONG_MAX) */ static volatile int g_ens_first; /* 1=首屏重头(offset=0) */
static volatile int g_ens_err; /* 最近一次滑窗补载错误(0=ok;首屏错误走 g_load_err) */
static void pl_drag_reset(void) static void pl_drag_reset(void)
{ {
@@ -44,15 +47,24 @@ static void pl_drag_reset(void)
static int pl_src2(int idx, meow_list_item *out, void *ctx) static int pl_src2(int idx, meow_list_item *out, void *ctx)
{ {
(void)ctx; (void)ctx;
if (idx < 0 || idx >= g_st.song_count) return 0; if (idx < 0 || idx >= g_st.track_ids_count) return 0;
ncm_song_t *s = &g_st.songs[idx]; static ncm_song_t s_s; /* 锁内拷贝驻留缓冲(src2 由 UI 线程串行调用,title 指针对当次绘制有效) */
if (appstate_song_copy(idx, &s_s) != 0) {
/* 占位行:滑窗未覆盖/未物化(后台 ensure 完成后自愈)。 */
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = "···";
out->subtitle = "加载中";
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
return 1;
}
static char sub[256]; static char sub[256];
if (s->art_name[0] && s->album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s->art_name, s->album_name); if (s_s.art_name[0] && s_s.album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s_s.art_name, s_s.album_name);
else if (s->art_name[0]) snprintf(sub, sizeof sub, "%s", s->art_name); else if (s_s.art_name[0]) snprintf(sub, sizeof sub, "%s", s_s.art_name);
else if (s->album_name[0]) snprintf(sub, sizeof sub, "%s", s->album_name); else if (s_s.album_name[0]) snprintf(sub, sizeof sub, "%s", s_s.album_name);
else snprintf(sub, sizeof sub, "-"); else snprintf(sub, sizeof sub, "-");
out->icon_kind = MEOW_LIST_ICON_NONE; out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s->name; out->title = s_s.name;
out->subtitle = sub; out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW; out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1; out->on = 0; out->enabled = 1;
@@ -64,12 +76,12 @@ static void pl_row_bg(void *ctx)
{ {
(void)ctx; (void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return; if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0 || g_st.song_count == 0) return; if (g_load_err != 0 || g_st.track_ids_count == 0) return;
const meow_theme *th = meow_theme_get(); const meow_theme *th = meow_theme_get();
int item = APP_ROW_H; int item = APP_ROW_H;
int off = meow_listview_scroll(g_list); int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1; int first = off / item, last = (off + r_list.h) / item + 1;
if (last > g_st.song_count) last = g_st.song_count; if (last > g_st.track_ids_count) last = g_st.track_ids_count;
for (int i = first; i < last; i++) { for (int i = first; i < last; i++) {
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2; int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */ if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */
@@ -100,7 +112,7 @@ static void pl_draw(void *ctx)
int nmx = r_cov.x + r_cov.w + 12; int nmx = r_cov.x + r_cov.w + 12;
char meta[256]; char meta[256];
if (pl) snprintf(meta, sizeof meta, "创建者 · %d 首", pl->track_count); if (pl) snprintf(meta, sizeof meta, "创建者 · %d 首", pl->track_count);
else snprintf(meta, sizeof meta, "曲目 %d 首", g_st.song_count); else snprintf(meta, sizeof meta, "曲目 %d 首", g_st.track_ids_count);
/* 歌单名(截断到卡片右缘:按 UTF-8 字符合法边界逐字累加测宽)。 */ /* 歌单名(截断到卡片右缘:按 UTF-8 字符合法边界逐字累加测宽)。 */
{ {
int maxw = r_card.x + r_card.w - 16 - nmx; int maxw = r_card.x + r_card.w - 16 - nmx;
@@ -131,11 +143,11 @@ static void pl_draw(void *ctx)
meow_ui_fill_rect(r_cmt.x + r_cmt.w / 2 + 34, r_cmt.y + 10, 6, 6, pa->red); meow_ui_fill_rect(r_cmt.x + r_cmt.w / 2 + 34, r_cmt.y + 10, 6, 6, pa->red);
/* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */ /* 曲目行:红 mini-vinyl 缩略 + 按压红条(文本由 listview z=10 画) */
if (g_load_err == 0 && g_st.song_count > 0) { if (g_load_err == 0 && g_st.track_ids_count > 0) {
int item = APP_ROW_H, isz = 24; int item = APP_ROW_H, isz = 24;
int off = meow_listview_scroll(g_list); int off = meow_listview_scroll(g_list);
int first = off / item, last = (off + r_list.h) / item + 1; int first = off / item, last = (off + r_list.h) / item + 1;
if (last > g_st.song_count) last = g_st.song_count; if (last > g_st.track_ids_count) last = g_st.track_ids_count;
for (int i = first; i < last; i++) { for (int i = first; i < last; i++) {
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2; int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
@@ -154,7 +166,7 @@ static void pl_draw_empty(void *ctx)
{ {
(void)ctx; (void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return; if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err == 0 && g_st.song_count != 0) return; if (g_load_err == 0 && g_st.track_ids_count != 0) return;
if (g_plloading) { ncm_draw_empty(r_list, "加载中", "正在加载曲目..."); return; } if (g_plloading) { ncm_draw_empty(r_list, "加载中", "正在加载曲目..."); return; }
if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "登录已过期", "请重新登录"); if (g_load_err == NCM_ERR_AUTH) ncm_draw_empty(r_list, "登录已过期", "请重新登录");
else if (g_load_err) ncm_draw_empty(r_list, "加载失败", "点按重试"); else if (g_load_err) ncm_draw_empty(r_list, "加载失败", "点按重试");
@@ -166,18 +178,22 @@ static void pl_draw_footer(void *ctx)
(void)ctx; (void)ctx;
if (meow_ui_activity_active() != ACT_PLAYLIST) return; if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0) return; if (g_load_err != 0) return;
int total = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) int total = g_st.track_ids_count;
? g_st.playlists[g_st.active_playlist].track_count : g_st.song_count; if (total <= 0) return;
/* 视口位置 / 总数(滑窗模型下「已物化数」无用户意义,视口区间更直观) */
int first = meow_listview_scroll(g_list) / APP_ROW_H + 1;
int last = first + r_list.h / APP_ROW_H;
if (last > total) last = total;
char foot[96]; char foot[96];
snprintf(foot, sizeof foot, "%d/%d 首", g_st.song_count, total); snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total);
ncm_draw_footer(r_cont, foot); ncm_draw_footer(r_cont, foot);
} }
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(触底追加,防跳顶)。 */ /* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */
static void pl_apply(int reset_scroll) static void pl_apply(int reset_scroll)
{ {
if (g_list) { if (g_list) {
meow_listview_set_count(g_list, g_st.song_count); meow_listview_set_count(g_list, g_st.track_ids_count); /* 全量行数(占位行兜底未物化区) */
if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); } if (reset_scroll) { meow_listview_set_scroll(g_list, 0); meow_listview_set_sel(g_list, -1); }
} }
if (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) { if (g_tb && g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count) {
@@ -193,16 +209,16 @@ static void *playlist_worker(void *arg)
{ {
(void)arg; (void)arg;
int my_idx = g_st.active_playlist; int my_idx = g_st.active_playlist;
int off = g_ploffset; int first = g_ens_first;
int lo = g_ens_lo, hi = g_ens_hi;
int rc; int rc;
if (g_host_mock) rc = 0; if (g_host_mock) rc = 0;
else if (my_idx < 0) rc = NCM_ERR_ARG; else if (my_idx < 0) rc = NCM_ERR_ARG;
else rc = appstate_load_playlist_songs_page(my_idx, off, NCM_SONG_BATCH); else if (first) rc = appstate_load_playlist_songs_page(my_idx, 0, NCM_SONG_BATCH);
else rc = appstate_song_ensure_range(lo, hi);
pthread_mutex_lock(&g_plmu); pthread_mutex_lock(&g_plmu);
g_load_err = rc; if (first) g_load_err = rc; /* 首屏失败 → 整页空态(点按重试) */
/* 本页后仍有多余 trackIds(tail 未到)且未到内存上限 → 还有下一页;否则到屏尾。 */ else g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
g_pl_more = (rc == NCM_OK) && (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
g_plloading = 0; g_plloading = 0;
g_plloaded_idx = my_idx; g_plloaded_idx = my_idx;
g_plgen++; g_plgen++;
@@ -210,19 +226,23 @@ static void *playlist_worker(void *arg)
return NULL; return NULL;
} }
/* 触底加载下一页:仅当仍有下一页、未在载、未失败、未到 NCM_SONG_MAX 时发。 */ /* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */
static void pl_maybe_load_more(void) static void pl_maybe_ensure(void)
{ {
if (g_plthr_on || g_host_mock) return; if (g_plthr_on || g_host_mock) return;
if (g_load_err != 0) return; /* 失败态:走点按重试,不自动循环 */ if (g_load_err != 0) return; /* 首屏失败态:走点按重试 */
if (!g_pl_more) return; int total = g_st.track_ids_count;
if (g_st.song_count >= NCM_SONG_MAX) return; if (total <= 0) return;
/* 视口底接近列表底(最后一行即将可见即预取) */ int scroll = meow_listview_scroll(g_list);
int list_px = g_st.song_count * APP_ROW_H; int first = scroll / APP_ROW_H;
int max_scroll = list_px - r_list.h; int last = (scroll + r_list.h) / APP_ROW_H + 1;
if (max_scroll < 0) max_scroll = 0; int lo = first - 24, hi = last + 24;
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return; if (lo < 0) lo = 0;
g_ploffset = g_st.song_count; /* 下一页起始 = 已物化数 */ if (hi > total) hi = total;
if (lo >= hi) return;
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作(窗口原地不动) */
g_ens_lo = lo; g_ens_hi = hi;
g_ens_first = 0;
g_plloading = 1; g_plloading = 1;
g_plthr_on = 1; g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) { if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
@@ -232,13 +252,11 @@ static void pl_maybe_load_more(void)
void playlist_refresh(void) void playlist_refresh(void)
{ {
if (g_host_mock) { g_load_err = 0; g_pl_more = 0; pl_apply(1); return; } if (g_host_mock) { g_load_err = 0; g_ens_err = 0; pl_apply(1); return; }
if (g_plthr_on) return; if (g_plthr_on) return;
g_load_err = 0; g_load_err = 0;
g_st.song_count = 0; g_ens_err = 0;
g_st.track_ids_count = 0; g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_ploffset = 0;
g_pl_more = 0;
g_plloading = 1; g_plloading = 1;
g_plthr_on = 1; g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) { if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
@@ -257,20 +275,15 @@ void playlist_tick(uint64_t now)
g_plloading = 0; g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; } if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
if (g_load_err != 0) { if (g_load_err != 0) {
if (g_ploffset == 0) { g_st.track_ids_count = 0; /* 首屏失败 → 整页空态(点按重试) */
g_st.song_count = 0; /* 首屏失败 → 整页空态(点按重试) */ pl_apply(1);
pl_apply(1);
} else {
/* 追加失败 → 保留已载歌曲 + 重置 g_pl_more(触底/点击可重试) */
ncm_snack("加载失败");
g_load_err = 0;
g_pl_more = (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
pl_apply(0);
}
return; return;
} }
pl_apply(g_ploffset == 0); /* 首屏回顶;触底追加保持滚动 */ if (g_ens_err != 0) {
ncm_snack("加载失败");
g_ens_err = 0; /* 占位行保留:滚动/点按触发重试 */
}
pl_apply(g_ens_first);
} }
void playlist_open(int idx) void playlist_open(int idx)
@@ -307,7 +320,7 @@ int playlist_on_tap(int x, int y)
playlist_refresh(); playlist_refresh();
return 1; return 1;
} }
if (g_st.song_count == 0) return 0; /* 空歌单(无失败):不重试 */ if (g_st.track_ids_count == 0) return 0; /* 空歌单(无失败):不重试 */
if (meow_widget_hit(g_list, x, y)) { if (meow_widget_hit(g_list, x, y)) {
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的 /* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
* meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行, * meow_listview_hit(x,y) 用“释放点 + 更新后的 scroll”命中 → 打到“已移动到手指下”的行,
@@ -317,7 +330,7 @@ int playlist_on_tap(int x, int y)
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y); int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
pl_drag_reset(); pl_drag_reset();
if (row < 0 || was_drag) return 1; if (row < 0 || was_drag) return 1;
player_play_song(row); player_play_song(row); /* 占位行也可点:播放线程经 appstate_song_fetch 物化该曲 */
return 1; return 1;
} }
return 0; return 0;
@@ -326,7 +339,7 @@ int playlist_on_tap(int x, int y)
void playlist_touch_down(int x, int y) void playlist_touch_down(int x, int y)
{ {
pl_drag_reset(); pl_drag_reset();
if (g_load_err != 0 || g_st.song_count == 0) return; if (g_load_err != 0 || g_st.track_ids_count == 0) return;
if (meow_widget_hit(g_list, x, y)) { if (meow_widget_hit(g_list, x, y)) {
int row = meow_listview_hit(g_list, x, y); int row = meow_listview_hit(g_list, x, y);
if (row >= 0) g_press_row = row; if (row >= 0) g_press_row = row;
@@ -350,20 +363,20 @@ void playlist_touch_move(int x, int y)
meow_listview_scroll_by(g_list, -dy); meow_listview_scroll_by(g_list, -dy);
if (meow_listview_scroll(g_list) != before) { if (meow_listview_scroll(g_list) != before) {
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
pl_maybe_load_more(); pl_maybe_ensure();
} }
} }
void playlist_swipe(int dir, int dist) void playlist_swipe(int dir, int dist)
{ {
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return; if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
if (g_load_err != 0 || g_st.song_count == 0) return; if (g_load_err != 0 || g_st.track_ids_count == 0) return;
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist; int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
int before = meow_listview_scroll(g_list); int before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del); meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before) { if (meow_listview_scroll(g_list) != before) {
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
pl_maybe_load_more(); pl_maybe_ensure();
} }
} }
+5 -3
View File
@@ -94,7 +94,9 @@ static void sr_apply(void)
meow_ui_paint_full(); meow_ui_paint_full();
} }
/* 后台 worker:唯一网络调用(cloudsearch),UI 不卡。 */ /* 后台 worker:唯一网络调用(cloudsearch),UI 不卡。
* §17:结果集(≤NCM_SEARCH_MAX,远小于窗口)经 appstate_adopt_songs 收编为窗口 base=0
* 播放上下文(song_ids/track_ids_count 同步),播放/上下一首对结果集生效。 */
static void *search_worker(void *arg) static void *search_worker(void *arg)
{ {
(void)arg; (void)arg;
@@ -104,12 +106,12 @@ static void *search_worker(void *arg)
g_st.songs, NCM_SEARCH_MAX, &n); g_st.songs, NCM_SEARCH_MAX, &n);
pthread_mutex_lock(&g_mu); pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) { if (rc == NCM_OK) {
appstate_adopt_songs(n); /* 收编 songs[0..n) 为窗口(写侧在锁内) */
g_st.search_count = n; g_st.search_count = n;
g_st.song_count = n; /* 结果集作为当前播放上下文 */
g_st.search_err = 0; g_st.search_err = 0;
} else { } else {
appstate_adopt_songs(0);
g_st.search_count = 0; g_st.search_count = 0;
g_st.song_count = 0;
g_st.search_err = rc; g_st.search_err = rc;
} }
g_loading = 0; g_loading = 0;
+48 -34
View File
@@ -1,19 +1,23 @@
/* bench/pagination_sim.c — 三期第一批 歌单分页 离线状态机验证(0 网络请求)。 /* bench/pagination_sim.c — 三期第三批 滑动窗口 基本语义离线回归(0 网络请求)。
* (§14 版为「分页 append 到 NCM_SONG_MAX=1000 截断」回归;§17 起分页被滑动窗口取代,
* 本文件随之改测兼容入口 appstate_load_playlist_songs_page 的新语义。)
* *
* 直接驱动 App 的真实函数 appstate_load_playlist_songs_page()(app/logic/appstate.c), * 直接驱动 App 的真实函数 appstate_load_playlist_songs_page()(app/logic/appstate.c),
* 仅把两个网络端点(ncm_api_playlist_detail / ncm_api_song_detail)替换成本地 stub * 仅把两个网络端点(ncm_api_playlist_detail / ncm_api_song_detail)替换成本地 stub
* (合成 trackIds / 合成歌曲对象),验证 detail 后按页 song_detail 的累加/封顶/尾部语义: * (合成 trackIds / 合成歌曲对象),验证:
* ① offset=0 重建索引(song_ids[]←全量 trackIds,track_ids_count)并载第 0 页; * ① offset=0 重建索引(song_ids[]←全量 trackIds,track_ids_count)并物化首段;
* ② 逐页 offset 前进,song_count 单调累加; * ② offset>0 物化 [offset,offset+limit)(滑窗 + song_detail 批补载);
* ③ 尾部(song_count==track_ids_count)不再载下一页;④ NCM_SONG_MAX 封顶不越界。 * ③ 尾部(offset≥track_ids_count)无操作;④ 大歌单不再截断(索引全量 5191,
* 风控纪律:本验证 0 次真实 NCM 请求(离线 stub)。歌曲上限 = NCM_SONG_MAX(内存封顶); * 窗口内有效槽位 ≤ NCM_SONG_WINDOW);⑤ 占位/出窗槽 song_copy 失败、入窗成功。
* 本模拟用两种歌单规模:(a) 130 首(< 上限,读全程);(b) 5191 首(> 上限,读满 NCM_SONG_MAX 截断)。 * 风控纪律:本验证 0 次真实 NCM 请求(离线 stub;cache_dir 置空 → songmeta 全部 miss/不落盘)。
* *
* 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c bench/pagination_sim.c -o /tmp/ps.o * -Icore -I../vendor/jsmn -c bench/pagination_sim.c -o /tmp/ps.o
* $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ps_app.o * -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ps_app.o
* $CC_HOST -O2 -Wl,--gc-sections -o pagination_sim /tmp/ps.o /tmp/ps_app.o -lpthread * $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c app/logic/songmeta.c -o /tmp/ps_meta.o
* $CC_HOST -O2 -Wl,--gc-sections -o pagination_sim /tmp/ps.o /tmp/ps_app.o /tmp/ps_meta.o -lpthread
* 运行:./pagination_sim (0 网络;PASS/FAIL 退出码) * 运行:./pagination_sim (0 网络;PASS/FAIL 退出码)
*/ */
#include "../app/ncm_app_internal.h" #include "../app/ncm_app_internal.h"
@@ -21,7 +25,7 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
/* appstate.c 依赖的全局(app.c 提供;本 bench 只测分页路径,仅 g_st 被用到) */ /* appstate.c 依赖的全局(app.c 提供;本 bench 只测窗口路径,仅 g_st 被用到) */
ncm_app_state_t g_st; ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 }; ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0; volatile int g_app_quit = 0;
@@ -49,7 +53,7 @@ int ncm_api_playlist_detail(const char *device_id, const char *cookie, const cha
char *name, size_t name_sz, char *cover, size_t cover_sz, char *name, size_t name_sz, char *cover, size_t cover_sz,
int64_t *ids, int id_cap, int *out_id_n) int64_t *ids, int id_cap, int *out_id_n)
{ {
(void)device_id; (void)cookie; (void)cover_sz; (void)device_id; (void)cookie; (void)cover_sz; (void)playlist_id;
g_detail_calls++; g_detail_calls++;
if (name) snprintf(name, name_sz, "Sim Playlist"); if (name) snprintf(name, name_sz, "Sim Playlist");
if (cover) cover[0] = 0; if (cover) cover[0] = 0;
@@ -75,7 +79,7 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
return NCM_OK; return NCM_OK;
} }
/* ---- 驱动:整批载入到尾部/上限,返回最终 song_count ---- */ /* ---- 驱动:重头 + 逐段 ensure 到尾部,返回 track_ids_count ---- */
static int load_all(int pid_total, int *detail_calls, int *song_calls) static int load_all(int pid_total, int *detail_calls, int *song_calls)
{ {
memset(&g_st, 0, sizeof g_st); memset(&g_st, 0, sizeof g_st);
@@ -87,17 +91,13 @@ static int load_all(int pid_total, int *detail_calls, int *song_calls)
int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH); int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) { printf(" page0 rc=%d\n", rc); return -1; } if (rc != NCM_OK) { printf(" page0 rc=%d\n", rc); return -1; }
int off = g_st.song_count; for (int off = NCM_SONG_BATCH; off < g_st.track_ids_count; off += NCM_SONG_BATCH) {
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
rc = appstate_load_playlist_songs_page(0, off, NCM_SONG_BATCH); rc = appstate_load_playlist_songs_page(0, off, NCM_SONG_BATCH);
if (rc != NCM_OK) { printf(" page(%d) rc=%d\n", off, rc); return -1; } if (rc != NCM_OK) { printf(" page(%d) rc=%d\n", off, rc); return -1; }
int nxt = g_st.song_count;
if (nxt <= off) break;
off = nxt;
} }
if (detail_calls) *detail_calls = g_detail_calls; if (detail_calls) *detail_calls = g_detail_calls;
if (song_calls) *song_calls = g_song_calls; if (song_calls) *song_calls = g_song_calls;
return g_st.song_count; return g_st.track_ids_count;
} }
static int g_fail = 0; static int g_fail = 0;
@@ -108,36 +108,50 @@ static int g_fail = 0;
int main(void) int main(void)
{ {
/* (a) 130 首(< NCM_SONG_MAX):应读全程,track_ids_count=130,song_count=130,尾部停。 */ printf("ncm_song_t=%zuB window=%zuKB(%d首) trackIds idx=%zuKB(%d首)\n",
printf("== case A: 130 songs (reads to tail) ==\n"); sizeof(ncm_song_t),
sizeof g_st.songs / 1024, NCM_SONG_WINDOW,
sizeof g_st.song_ids / 1024, NCM_TRACKID_MAX);
/* (a) 130 首(< 窗口):读全程后 130 槽全有效(窗口装得下,无需滑窗)。 */
printf("== case A: 130 songs (whole list fits window) ==\n");
int dc = 0, sc = 0; int dc = 0, sc = 0;
int n = load_all(130, &dc, &sc); int n = load_all(130, &dc, &sc);
CHECK(n == 130, "song_count=%d (expect 130)", n); CHECK(n == 130, "track_ids_count=%d (expect 130)", n);
CHECK(g_st.track_ids_count == 130, "track_ids_count=%d (expect 130)", g_st.track_ids_count); CHECK(g_st.song_count == 130, "valid slots=%d (expect 130, all in window)", g_st.song_count);
CHECK(g_st.window_base == 0, "window_base=%d (expect 0)", g_st.window_base);
CHECK(dc == 1, "detail calls=%d (expect 1, index fetched once)", dc); CHECK(dc == 1, "detail calls=%d (expect 1, index fetched once)", dc);
CHECK(sc == 2, "song_detail calls=%d (expect 2 = 100 + 30 pages)", sc); CHECK(sc == 2, "song_detail calls=%d (expect 2 = batches 0-99 + 100-129)", sc);
CHECK(strcmp(g_st.songs[129].name, "Song1000129") == 0, "last song name='%s'", g_st.songs[129].name); ncm_song_t sq;
CHECK(g_st.songs[129].al_pic[0] != 0, "last song has al_pic='%s'", g_st.songs[129].al_pic); CHECK(appstate_song_copy(129, &sq) == 0 && strcmp(sq.name, "Song1000129") == 0,
"last song copy name='%s'", sq.name);
CHECK(sq.al_pic[0] != 0, "last song has al_pic='%s'", sq.al_pic);
/* 越过尾部再 load:应无操作(不越界) */ /* 越过尾部再 load:应无操作(不越界) */
int rc = appstate_load_playlist_songs_page(0, 130, NCM_SONG_BATCH); int rc = appstate_load_playlist_songs_page(0, 130, NCM_SONG_BATCH);
CHECK(rc == NCM_OK && g_st.song_count == 130, "load past tail: rc=%d song_count=%d", rc, g_st.song_count); CHECK(rc == NCM_OK && g_st.song_count == 130, "load past tail: rc=%d valid=%d", rc, g_st.song_count);
/* (b) 5191 首(> NCM_SONG_MAX):索引封顶 NCM_SONG_MAX,读满封顶截断。 */ /* (b) 5191 首(>> 窗口):索引全量不截断(§17 取消 NCM_SONG_MAX=1000 上限),
printf("== case B: 5191 songs (capped at NCM_SONG_MAX) ==\n"); * 窗口有效槽恒 ≤ NCM_SONG_WINDOW,末段滑窗后末首可达。 */
printf("== case B: 5191 songs (no cap; window slides) ==\n");
dc = sc = 0; dc = sc = 0;
n = load_all(5191, &dc, &sc); n = load_all(5191, &dc, &sc);
CHECK(n == NCM_SONG_MAX, "song_count=%d (expect NCM_SONG_MAX=%d)", n, NCM_SONG_MAX); CHECK(n == 5191, "track_ids_count=%d (expect 5191, 不再 1000 截断)", n);
CHECK(g_st.track_ids_count == NCM_SONG_MAX, "track_ids_count=%d (index capped)", g_st.track_ids_count); CHECK(g_st.song_count <= NCM_SONG_WINDOW, "valid slots=%d ≤ window %d", g_st.song_count, NCM_SONG_WINDOW);
CHECK(dc == 1, "detail calls=%d", dc); CHECK(dc == 1, "detail calls=%d", dc);
CHECK(sc == NCM_SONG_MAX / NCM_SONG_BATCH, "song_detail calls=%d (expect %d full pages)", sc, NCM_SONG_MAX / NCM_SONG_BATCH); CHECK(sc == (5191 + NCM_SONG_BATCH - 1) / NCM_SONG_BATCH,
/* 越界再 load:应 NCM_OK 但不动 song_count */ "song_detail calls=%d (expect %d grid batches)", sc, (5191 + NCM_SONG_BATCH - 1) / NCM_SONG_BATCH);
rc = appstate_load_playlist_songs_page(0, NCM_SONG_MAX, NCM_SONG_BATCH); CHECK(appstate_song_copy(5190, &sq) == 0 && strcmp(sq.name, "Song1005190") == 0,
CHECK(rc == NCM_OK && g_st.song_count == NCM_SONG_MAX, "load past cap: rc=%d song_count=%d", rc, g_st.song_count); "tail song copy name='%s'", sq.name);
CHECK(appstate_song_copy(0, &sq) != 0, "head song evicted (out of window) -> copy fails");
/* 越界再 load:应 NCM_OK 但不动窗口 */
int valid_before = g_st.song_count;
rc = appstate_load_playlist_songs_page(0, 5191, NCM_SONG_BATCH);
CHECK(rc == NCM_OK && g_st.song_count == valid_before, "load past tail: rc=%d valid=%d", rc, g_st.song_count);
/* (c) 空歌单(0 首) */ /* (c) 空歌单(0 首) */
printf("== case C: empty playlist ==\n"); printf("== case C: empty playlist ==\n");
n = load_all(0, &dc, &sc); n = load_all(0, &dc, &sc);
CHECK(n == 0 && g_st.track_ids_count == 0, "empty: song_count=%d track_ids_count=%d", n, g_st.track_ids_count); CHECK(n == 0 && g_st.song_count == 0, "empty: track_ids_count=%d valid=%d", n, g_st.song_count);
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS"); printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0; return g_fail ? 1 : 0;
+231
View File
@@ -0,0 +1,231 @@
/* bench/window_sim.c — 三期第三批 滑动窗口 6000 首滚动全程离线验证(0 网络请求)。
*
* 驱动真实 appstate.c(滑动窗口)+ 真实 songmeta.c(盘缓存,/tmp 临时目录),
* 网络端点(ncm_api_playlist_detail / ncm_api_song_detail)为本地合成 stub。
* 场景(对齐用户「我喜欢」6000+ 首):
* ① 重头:detail 取全量 6000 trackIds,物化首段(1 detail + 1 song_detail 批);
* ② 全程下滚:视口 12 行、步进 8 行(模拟跟手拖动),每步 pl_maybe_ensure 等价物
* ensure(pos-24, pos+36);每步断言视口 12 行全部物化(列表无占位破窗);
* 断言 song_detail 调用数 ≈ 6000/批网格(批对齐预取,无小请求放大);
* ③ 全程回滚到 0:盘缓存(songmeta)全命中,song_detail 调用数 0 增长;
* ④ 播放兼容:窗口感知的取歌——播放线程路径 appstate_song_fetch 在窗口远离时
* 仍能物化当前播放曲(不滑窗、不崩、盘缓存命中免请求);
* ⑤ 尾部/越界:ensure 尾部区间 + 越过尾部无操作;
* ⑥ 内存账:6000 首场景改前(1000 截断 × 1324B)vs 改后(窗口 256×608B + 索引)。
* 风控纪律:0 次真实 NCM 请求(离线 stub + /tmp 盘缓存)。
*
* 编译(宿主):$CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c bench/window_sim.c -o /tmp/ws.o
* $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c app/logic/appstate.c -o /tmp/ws_app.o
* $CC_HOST -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -I../vendor/jsmn -c app/logic/songmeta.c -o /tmp/ws_meta.o
* $CC_HOST -O2 -Wl,--gc-sections -o window_sim /tmp/ws.o /tmp/ws_app.o /tmp/ws_meta.o -lpthread
* 运行:./window_sim (0 网络;PASS/FAIL 退出码)
*/
#include "../app/ncm_app_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 0;
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
{
size_t n = 0;
if (sz) {
while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; }
dst[n] = 0;
}
while (src[n]) n++;
return n;
}
/* ---- 网络端点 stub(离线,合成数据) ---- */
#define SIM_TOTAL 6000
static int g_detail_calls;
static int g_song_calls;
static int g_song_served; /* song_detail 累计返回首数 */
int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id,
char *name, size_t name_sz, char *cover, size_t cover_sz,
int64_t *ids, int id_cap, int *out_id_n)
{
(void)device_id; (void)cookie; (void)cover_sz; (void)playlist_id;
g_detail_calls++;
if (name) snprintf(name, name_sz, "Sim Playlist");
if (cover) cover[0] = 0;
int n = (SIM_TOTAL < id_cap) ? SIM_TOTAL : id_cap;
for (int i = 0; i < n; i++) ids[i] = 2000000LL + i;
*out_id_n = n;
return NCM_OK;
}
int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t *ids, int n,
ncm_song_t *out, int max, int *out_n)
{
(void)device_id; (void)cookie;
g_song_calls++;
int m = (n < max) ? n : max;
g_song_served += m;
for (int i = 0; i < m; i++) {
snprintf(out[i].id, sizeof out[i].id, "%lld", (long long)ids[i]);
snprintf(out[i].name, sizeof out[i].name, "Song%lld", (long long)ids[i]);
snprintf(out[i].art_name, sizeof out[i].art_name, "Artist%lld", (long long)ids[i]);
snprintf(out[i].album_name, sizeof out[i].album_name, "Album%lld", (long long)ids[i]);
snprintf(out[i].al_pic, sizeof out[i].al_pic, "http://p.music.126.net/cov%lld.jpg", (long long)ids[i]);
out[i].duration_ms = 200000;
}
*out_n = m;
return NCM_OK;
}
static int g_fail = 0;
#define CHECK(cond, fmt, ...) do { \
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
} while (0)
/* 模拟 playlist.c pl_maybe_ensure:视口 12 行 ±24 行余量。 */
static int ui_ensure(int pos)
{
int lo = pos - 24, hi = pos + 12 + 24;
if (lo < 0) lo = 0;
if (hi > SIM_TOTAL) hi = SIM_TOTAL;
if (lo >= hi) return NCM_OK;
if (appstate_range_loaded(lo, hi)) return NCM_OK;
return appstate_song_ensure_range(lo, hi);
}
int main(void)
{
if (system("rm -rf /tmp/ncm_window_sim_cache") != 0) { /* 清上次运行残留(盘缓存) */
printf("FAIL cannot clean /tmp cache dir\n");
return 1;
}
memset(&g_st, 0, sizeof g_st);
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_window_sim_cache");
g_st.playlist_count = 1;
snprintf(g_st.playlists[0].id, sizeof g_st.playlists[0].id, "pl6000");
g_st.playlists[0].track_count = SIM_TOTAL;
size_t win_bytes = sizeof g_st.songs + sizeof g_st.song_valid;
size_t idx_bytes = sizeof g_st.song_ids;
size_t old_bytes = (size_t)1000 * 1324; /* §14:NCM_SONG_MAX=1000 × ~1324B/首 */
printf("== memory model ==\n");
printf(" ncm_song_t=%zuB (old 1324B) window=%zuB(%d首) trackIds idx=%zuB(%d首)\n",
sizeof(ncm_song_t), win_bytes, NCM_SONG_WINDOW, idx_bytes, NCM_TRACKID_MAX);
printf(" 6000首场景:旧模型 %zuKB(且 1000 截断)→ 新模型 %zuKB(任意长度恒定)\n",
old_bytes / 1024, (win_bytes + idx_bytes) / 1024);
/* ① 重头 */
printf("== phase 1: open 6000-song playlist ==\n");
int rc = appstate_load_playlist_songs_page(0, 0, NCM_SONG_BATCH);
CHECK(rc == NCM_OK, "page0 rc=%d", rc);
CHECK(g_st.track_ids_count == SIM_TOTAL, "track_ids_count=%d (expect %d, 无截断)",
g_st.track_ids_count, SIM_TOTAL);
CHECK(g_st.song_count == NCM_SONG_BATCH, "valid slots=%d (expect first batch %d)",
g_st.song_count, NCM_SONG_BATCH);
CHECK(g_st.window_base == 0, "window_base=%d", g_st.window_base);
CHECK(g_detail_calls == 1 && g_song_calls == 1, "detail=%d song_detail=%d (expect 1+1)",
g_detail_calls, g_song_calls);
/* ② 全程下滚(0→6000,步进 8 行):视口每步全部物化 */
printf("== phase 2: scroll down 0 -> %d (step 8 rows) ==\n", SIM_TOTAL);
int step_checks = 0;
for (int pos = 0; pos < SIM_TOTAL; pos += 8) {
rc = ui_ensure(pos);
if (rc != NCM_OK) { printf(" FAIL ensure(pos=%d) rc=%d\n", pos, rc); g_fail = 1; break; }
int vhi = pos + 12; if (vhi > SIM_TOTAL) vhi = SIM_TOTAL;
if (!appstate_range_loaded(pos, vhi)) {
printf(" FAIL viewport [%d,%d) not fully materialized\n", pos, vhi);
g_fail = 1; break;
}
step_checks++;
}
CHECK(step_checks == (SIM_TOTAL + 7) / 8, "scroll steps verified=%d (expect %d)",
step_checks, (SIM_TOTAL + 7) / 8);
CHECK(g_st.song_count <= NCM_SONG_WINDOW, "valid slots=%d ≤ window %d (全程不超窗)",
g_st.song_count, NCM_SONG_WINDOW);
printf(" INFO song_detail calls=%d served=%d (grid prefetch; 下限 %d)\n",
g_song_calls, g_song_served, SIM_TOTAL / NCM_SONG_BATCH);
CHECK(g_song_calls <= SIM_TOTAL / NCM_SONG_BATCH + 6,
"song_detail calls=%d ≤ %d (批网格对齐,无小请求放大)",
g_song_calls, SIM_TOTAL / NCM_SONG_BATCH + 6);
ncm_song_t sq;
CHECK(appstate_song_copy(SIM_TOTAL - 1, &sq) == 0 &&
strcmp(sq.name, "Song2005999") == 0, "tail song name='%s'", sq.name);
CHECK(appstate_song_copy(0, &sq) != 0, "head evicted after scrolling to tail");
/* ③ 全程回滚到 0:盘缓存全命中,0 新增 song_detail */
printf("== phase 3: scroll back up %d -> 0 (disk cache) ==\n", SIM_TOTAL);
int calls_before = g_song_calls;
step_checks = 0;
for (int pos = SIM_TOTAL - 8; pos >= 0; pos -= 8) {
rc = ui_ensure(pos);
if (rc != NCM_OK) { printf(" FAIL ensure(pos=%d) rc=%d\n", pos, rc); g_fail = 1; break; }
int vhi = pos + 12; if (vhi > SIM_TOTAL) vhi = SIM_TOTAL;
if (!appstate_range_loaded(pos, vhi)) {
printf(" FAIL viewport [%d,%d) not materialized on way back\n", pos, vhi);
g_fail = 1; break;
}
step_checks++;
}
CHECK(g_song_calls == calls_before,
"scroll-back song_detail calls +%d (expect 0,盘缓存全命中)", g_song_calls - calls_before);
CHECK(appstate_song_copy(0, &sq) == 0 && strcmp(sq.name, "Song2000000") == 0,
"head re-materialized from disk: name='%s'", sq.name);
/* ④ 播放兼容:窗口滑出当前播放曲 → fetch 不滑窗、不崩、盘命中免请求 */
printf("== phase 4: playback survives window slide ==\n");
rc = appstate_song_ensure_range(3000 - 128, 3000 + 128); /* 窗口移到 3000 附近 */
int base_at_play = g_st.window_base;
CHECK(rc == NCM_OK, "ensure around 3000 rc=%d", rc);
/* 模拟 run_song:cur=3050 在窗内 → song_copy;然后窗口滑回 0,再 fetch(3050) */
CHECK(appstate_song_copy(3050, &sq) == 0, "song_copy(3050) in window");
rc = appstate_song_ensure_range(0, 100); /* 用户滚回顶部(窗口滑走) */
CHECK(rc == NCM_OK && g_st.window_base != base_at_play,
"window slid away base %d -> %d", base_at_play, g_st.window_base);
CHECK(appstate_song_copy(3050, &sq) != 0, "playing song evicted from window");
calls_before = g_song_calls;
rc = appstate_song_fetch(3050, &sq); /* 播放线程路径:不滑窗 */
CHECK(rc == NCM_OK && strcmp(sq.id, "2003050") == 0 && sq.duration_ms == 200000,
"fetch(3050) ok id=%s dur=%d", sq.id, sq.duration_ms);
CHECK(g_st.window_base != base_at_play && g_st.song_count <= NCM_SONG_WINDOW,
"fetch did not move window (base=%d)", g_st.window_base);
CHECK(g_song_calls == calls_before, "fetch hit disk cache (0 new requests)");
/* next/prev 边界语义:cur 可到 SIM_TOTAL-1,不越界不崩 */
CHECK(appstate_song_fetch(SIM_TOTAL - 1, &sq) == NCM_OK, "fetch(tail) ok");
CHECK(appstate_song_fetch(SIM_TOTAL, &sq) != NCM_OK, "fetch(out of range) rejected");
/* ⑤ 尾部/越界 ensure */
printf("== phase 5: tail & out-of-range ==\n");
rc = appstate_song_ensure_range(SIM_TOTAL - 12, SIM_TOTAL);
CHECK(rc == NCM_OK && appstate_range_loaded(SIM_TOTAL - 12, SIM_TOTAL),
"tail ensure rc=%d", rc);
CHECK(g_st.window_base <= SIM_TOTAL - NCM_SONG_WINDOW ||
SIM_TOTAL < NCM_SONG_WINDOW, "base=%d clamped at tail", g_st.window_base);
rc = appstate_song_ensure_range(SIM_TOTAL, SIM_TOTAL + 100);
CHECK(rc == NCM_OK, "ensure past tail no-op rc=%d", rc);
rc = appstate_song_ensure_range(-50, 0);
CHECK(rc == NCM_OK, "ensure empty range no-op rc=%d", rc);
/* ⑥ 盘缓存规模核对 */
printf("== phase 6: disk cache ==\n");
{
char cmd[256];
snprintf(cmd, sizeof cmd,
"ls /tmp/ncm_window_sim_cache/meta | wc -l");
FILE *p = popen(cmd, "r");
int files = -1;
if (p) { if (fscanf(p, "%d", &files) != 1) files = -1; pclose(p); }
CHECK(files == SIM_TOTAL, "songmeta files=%d (expect %d, 全程拉过即落盘)", files, SIM_TOTAL);
}
printf("%s\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS");
return g_fail ? 1 : 0;
}
+1 -1
View File
@@ -19,7 +19,7 @@ CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
core/ncm_cache.c core/ncm_ctx.c" core/ncm_cache.c core/ncm_ctx.c"
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \ APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \ app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c \ app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \ app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c" app/play/ipc_media.c app/play/minimp3_impl.c"
+1 -1
View File
@@ -31,7 +31,7 @@ $CC_HOST -O2 -c -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" "$SDK_DIR/test_ho
echo "==> host app sources (exclude app.c: has main + g_st; test provides them)" echo "==> host app sources (exclude app.c: has main + g_st; test provides them)"
APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \ APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \ app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c \ app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \ app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c" app/play/ipc_media.c app/play/minimp3_impl.c"
for f in $APP_SRC; do for f in $APP_SRC; do
+11 -6
View File
@@ -33,13 +33,18 @@ typedef struct ncm_playlist {
int is_collected; /* 1=我收藏的(订阅他人);0=我创建的。来自响应 subscribed(§7.0) */ int is_collected; /* 1=我收藏的(订阅他人);0=我创建的。来自响应 subscribed(§7.0) */
} ncm_playlist_t; } ncm_playlist_t;
/* 瘦身(REPORT §17):旧版 name/art/al 各 256 + al_pic 512 + long dt ≈ 1324B/首,
* 滑动窗口常驻 256 首 ≈331KB 太肥;裁到 ≈608B/首(窗口 ≈152KB)。id 为数字文本
* (int64 最大 19 位 + NUL,24 足够);name 128 / art 96 / al 96(列表/播放页展示均按宽
* 再截断);al_pic 256(picUrl 实际 <200 字符,obj_str 截断安全);dt 用 int32_t
* (时长 <2^31ms≈24 天;MIPS32 的 long 本就 32 位)。字段变窄只影响展示截断,不改变语义。 */
typedef struct ncm_song { typedef struct ncm_song {
char id[32]; char id[24];
char name[256]; char name[128];
char art_name[256]; /* 首个歌手名 */ char art_name[96]; /* 首个歌手名 */
char album_name[256]; char album_name[96];
char al_pic[512]; /* 专辑封面 picUrl(song_detail/cloudsearch/playlist_track_all 解析;播放页黑胶封面) */ char al_pic[256]; /* 专辑封面 picUrl(song_detail/cloudsearch 解析;播放页黑胶封面) */
long duration_ms; int32_t duration_ms;
int fee; int fee;
} ncm_song_t; } ncm_song_t;
Binary file not shown.
Binary file not shown.
Binary file not shown.