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

+247 -41
View File
@@ -7,6 +7,9 @@
#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)
{
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 */
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
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.active_playlist = -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.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
meow_config_remove("ncm.uid");
meow_config_remove("ncm.nickname");
cover_cache_reset();
@@ -120,6 +128,219 @@ int appstate_load_playlists(void)
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)
{
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;
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
* 回到 §10.11 已验证路径并做**真分页**:detail(n=1)取【全量】trackIds 入 song_ids[],
* song_detail 按 ≤NCM_SONG_BATCH 分批拉歌曲对象(含 al.picUrl),offset 为「下一页」起始;
* 列表触底驱动 offset 前进,song_count 逐页累加(不再一次性拉满,大歌单不冻 UI,
* 也不把「第 N 页之后的歌」挡在 256 之外——NCM_SONG_MAX 上限提升到 1000,见 §14)。 */
* §14 的分页(append-only 到 NCM_SONG_MAX=1000 截断)在 §17 升级为滑动窗口:
* detail(n=1)仍取【全量】trackIds 入 song_ids[](≤ NCM_TRACKID_MAX,支撑 6000+ 首),
* 歌曲对象只物化视口附近的 NCM_SONG_WINDOW 首(offset 为「要确保的区间」起点)。 */
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.songs, 0, sizeof g_st.songs);
g_st.window_base = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
pthread_mutex_unlock(&g_song_mu);
int idn = 0;
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
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;
g_st.track_ids_count = idn;
/* 首屏只需第 0 页:song_detail 只拉前 limit 首,其余待滚动触底再拉(分页驱动)。 */
appstate_apply_playlist_cover(idx);
} else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG;
}
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部/上限 */
if (offset >= NCM_SONG_MAX) return NCM_OK; /* 内存上限:不再物化更多 */
int can = g_st.track_ids_count - offset;
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;
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部 */
int hi = offset + limit;
if (hi > g_st.track_ids_count) hi = g_st.track_ids_count;
return appstate_song_ensure_range(offset, hi);
}
int appstate_load_playlist_songs(int idx)
{
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
/* 兼容入口:载索引 + 物化首个窗口。 */
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int off = g_st.song_count;
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
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;
int hi = (g_st.track_ids_count < NCM_SONG_WINDOW) ? g_st.track_ids_count : NCM_SONG_WINDOW;
return appstate_song_ensure_range(0, hi);
}
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)
{
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;
}
@@ -227,18 +431,20 @@ static int autoplay_try_playlist(int pl)
return 0;
}
char cpath[600];
for (int i = 0; i < g_st.song_count; i++) {
if (ncm_cache_lookup(&g_st.ctx.cache, g_st.songs[i].id, ".mp3",
for (int i = 0; i < g_st.track_ids_count; i++) { /* 滑窗模型:锁内拷贝窗内有效槽 */
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) {
g_st.active_playlist = pl;
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);
return 1;
}
}
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first page %d songs, no cached track",
pl, g_st.song_count);
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first window %d/%d songs, no cached track",
pl, g_st.song_count, g_st.track_ids_count);
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)---- */
#define NCM_PLAYLIST_MAX 120 /* 我的歌单上限 */
/* 单个歌单曲目上限 = 分页驱动下的「单页渲染上限」(REPORT §14,三期第一批):
* detail(/v6/playlist/detail, n=1)取回全量 trackIds 入 song_ids[](≤ NCM_SONG_MAX);
* song_detail 按 NCM_SONG_BATCH 分批拉歌词对象,列表触底自动加载下一页,直到
* song_count == track_ids_count(歌单尾部)或 == NCM_SONG_MAX(内存上限,footer 显示 x/总数)。
* 不再硬截断 256(NCM_SONG_MAX 从 256 提升到 1000,约 1.32KB/首 → 歌曲数组 ~1.37MB,14MB 内安全)。
* 数值权衡见 REPORT §14.8(可再上调,代价是 bss 线性增长:每 +100 首 ≈ +132KB)。 */
#define NCM_SONG_MAX 1000 /* 歌单曲目/歌曲对象上限(append-only 分页加载) */
/* 滑动窗口(REPORT §17,三期第三批,取代 §14 的 NCM_SONG_MAX=1000 硬截断):
* song_ids[NCM_TRACKID_MAX] = detail(/v6/playlist/detail, n=1)取回的全量 trackIds
* (int64×16384=128KB bss,支撑任意长度歌单,不再 1000 截断);
* songs[NCM_SONG_WINDOW] = 物化滑动窗口(256 首 × ~608B ≈ 152KB bss,与歌单长度无关);
* song_valid[NCM_SONG_WINDOW]= 槽位有效位图(songs[i] ↔ 全局下标 window_base+i);
* 滚动出窗的槽位失效(数据已序列化到 <cache_dir>/meta/<id>.bin),入窗经 song_detail 按需
* 拉取(盘缓存命中免请求)。列表 count = track_ids_count(全量),占位行驱动后台 ensure。 */
#define NCM_TRACKID_MAX 16384 /* 全量 trackIds 索引上限(128KB bss) */
#define NCM_SONG_WINDOW 256 /* 滑动窗口大小(内存物化歌曲对象数上限) */
#define NCM_SONG_BATCH 100 /* song_detail 分批每批条数(≤500 安全窗内取保守 100;
* 同时作「下一页」加载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB */
* 窗口补载粒度;c 数组缓冲 clist[4096] 装 100 首 ≈2KB) */
#define NCM_LYRIC_LINES 600 /* 歌词行上限 */
#define NCM_COVER_MAX 288*1024 /* 封面下载限幅 ~288KB */
#define NCM_SEARCH_MAX 32 /* 搜索结果上限(v1 只显示前 32 条,无大缓冲) */
@@ -84,6 +86,10 @@ typedef struct ncm_play_run {
int err; /* 最近一次播放错误(0=无) */
char errmsg[96];
int has_track; /* 当前是否有可播曲目(含 url 获取中) */
/* 播放曲快照(REPORT §17):run_song 起播时把歌曲对象整体拷入(mu 保护),
* 之后播放/歌词/IPC/UI 全读快照而非窗口——窗口滑出当前播放曲也不影响播放。 */
ncm_song_t play_song;
int play_song_valid; /* 1=快照有效(stop 清 0) */
} ncm_play_run_t;
/* ---- 音质档位字符串(设置页 + 持久化用)---- */
@@ -113,14 +119,19 @@ typedef struct ncm_app_state {
int active_playlist; /* 当前打开歌单的下标;-1=无 */
/* 当前歌单曲目(id 用 int64_t:网易云歌曲 id 可 >2^31,MIPS32 long 溢出)。
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_SONG_MAX,分页索引);
* songs[] = 已物化的歌曲对象(append-only,与 song_ids[] 前 song_count 首一一对应);
* song_count = 已物化数(列表 count = song_count);track_ids_count = trackIds 总数(≥ song_count,
* 其差值 = 仍有「下一页」待 song_detail 分批拉取)。 */
int64_t song_ids[NCM_SONG_MAX];
ncm_song_t songs[NCM_SONG_MAX];
* 滑动窗口模型(REPORT §17):
* song_ids[] = detail 返回的全量 trackIds(≤ NCM_TRACKID_MAX,全局下标索引);
* songs[] = 物化滑动窗口,songs[i] ↔ 全局下标 window_base+i;
* song_valid[] = 槽位有效位图(出窗=失效,入窗=物化后置位);
* window_base = 窗口起始全局下标;song_count = 窗口内有效槽位数(≤ NCM_SONG_WINDOW);
* track_ids_count = trackIds 总数(列表 count = track_ids_count,任意长度无截断)。
* 窗口读写由 appstate.c 内 g_song_mu 保护;外部一律走 appstate_song_copy/ensure_range。 */
int64_t song_ids[NCM_TRACKID_MAX];
ncm_song_t songs[NCM_SONG_WINDOW];
uint8_t song_valid[NCM_SONG_WINDOW];
int window_base;
int song_count;
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_SONG_MAX) */
int track_ids_count; /* detail 取回的 trackIds 总数(≤ NCM_TRACKID_MAX) */
/* 播放页视图态 */
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);
int appstate_ensure_uid(void); /* 若 uid 缺失则 login_status_check 取回;0=成功 */
int appstate_load_playlists(void); /* 拉我的歌单 */
int appstate_load_playlist_songs(int idx); /* 兼容:整批载入(内部走分页循环) */
int appstate_load_playlist_songs(int idx); /* 兼容:载索引 + 首个窗口(内部走 ensure) */
int appstate_load_playlist_songs_page(int idx, int offset, int limit);
/* detail(全量 trackIds)后按页拉 song_detail:
* offset=0 重头(重建 song_ids[]/track_ids_count,清空后载第 0 页);
* offset>0 追加第 offset 页到 g_st.songs[offset..];
* 写 g_st.song_count(=offset+本页实际数)。 */
/* 兼容入口(滑动窗口语义,REPORT §17):
* offset=0 重头(detail 全量 trackIds 重建索引、清窗口、
* 物化 [0,limit));offset>0 = 物化 [offset,offset+limit)
* (滑窗+盘缓存+song_detail 补载,内部 ensure_range)。 */
int appstate_song_ensure_range(int lo, int hi);
/* 滑动窗口核心:确保全局下标 [lo,hi) 已物化。
* 窗口未覆盖先滑窗(重叠区保留,出窗槽位失效);
* 缺失槽位先读盘缓存(<cache_dir>/meta/<id>.bin),
* miss 攒批 ≤NCM_SONG_BATCH 走 song_detail,成功落盘。
* worker 线程调用(含网络);返回 NCM_OK/负错误码。 */
int appstate_range_loaded(int lo, int hi); /* 1=[lo,hi) 全部在窗内且有效(UI 判是否需 ensure) */
int appstate_song_copy(int gidx, ncm_song_t *out); /* 锁内读窗:0=已物化拷出;-1=窗外/未物化 */
int appstate_song_fetch(int gidx, ncm_song_t *out); /* 任意全局下标物化(窗→盘→song_detail 单曲),
* 不滑窗(播放线程用,避免与浏览视口抢窗口) */
void appstate_adopt_songs(int n); /* 收编外部已填的 songs[0..n)(搜索结果)为窗口 base=0,
* 同步 song_ids/track_ids_count(播放上下文) */
void appstate_apply_playlist_cover(int idx);
void appstate_set_playing_song(int idx, const char *cover_url);
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
@@ -311,6 +334,12 @@ int64_t player_manager_pos_ms(void);
int64_t player_manager_dur_ms(void);
int player_manager_load_lyric(void); /* 为当前曲目拉歌词并解析 */
int player_manager_error(void);
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
int songmeta_load(const char *cache_dir, const char *song_id, ncm_song_t *out); /* NCM_OK / NCM_ERR_NOENT */
int songmeta_save(const char *cache_dir, const char *song_id, const ncm_song_t *s);
/* ipc_media:媒体会话上报/收指令 */
int ipc_media_start(void); /* 起 IPC 线程;0=成功 */
+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];
title[0] = artist[0] = album[0] = 0;
if (idx >= 0 && idx < g_st.song_count) {
snprintf(title, sizeof title, "%s", g_st.songs[idx].name);
snprintf(artist, sizeof artist, "%s", g_st.songs[idx].art_name);
snprintf(album, sizeof album, "%s", g_st.songs[idx].album_name);
ncm_song_t sm;
if (idx >= 0 && player_manager_current_song(&sm) == 0) { /* 播放快照(窗口无关,§17) */
snprintf(title, sizeof title, "%s", sm.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.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; }
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)
{
g_level = level;
@@ -83,7 +95,7 @@ void player_manager_next(int dir)
pthread_mutex_lock(&g_play.mu);
int c = g_play.cur_song + dir;
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) {
g_play.cur_song = c;
g_play.cmd = 0;
@@ -100,6 +112,7 @@ void player_manager_stop(void)
g_play.cur_song = -1;
g_play.state = NCM_PC_STOPPED;
g_play.pos_ms = 0;
g_play.play_song_valid = 0; /* 快照失效 */
g_play.seq++;
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 降级 + 流式落缓存。
* 返回 NCM_OK 或负错误码。 */
static int song_local_file(int idx, char *path, size_t psz, int64_t *bytes)
* 返回 NCM_OK 或负错误码。id 由调用方(run_song)从播放曲快照取,与窗口解耦(§17)。 */
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) {
struct stat st;
@@ -176,9 +186,21 @@ static void set_err(int err, const char *msg)
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
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;
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) {
const char *m = (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)"
: (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);
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;
memset(&dec, 0, sizeof dec);
@@ -205,7 +226,7 @@ static void run_song(int idx)
g_play.dur_ms = dur;
g_play.err = 0;
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];
int64_t frames_done = 0; /* 已输出帧(每通道) */
@@ -291,7 +312,7 @@ static void *player_thread(void *arg)
int st = g_play.state;
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);
continue;
}
@@ -303,9 +324,9 @@ static void *player_thread(void *arg)
int st2 = g_play.state;
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;
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 {
usleep(50 * 1000);
@@ -316,23 +337,27 @@ static void *player_thread(void *arg)
/* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延
* 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ----
* 现存 race(§10.9 FM4,UI 读 lyrics 与 worker 写)为良性:数组定长、越界已防护,
* 最坏显示一帧旧/空行,下一 tick 自愈;与 g_st.songs 写读竞态同接受。 */
* §17:曲目 id 改读播放快照(g_play.play_song,mu 保护),不再依赖窗口内
* g_st.songs[]——窗口滑出当前播放曲歌词照常。 */
static pthread_mutex_t g_lyrmu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_lyr_thr;
static int g_lyr_thr_on;
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)
{
(void)arg;
char last_id[32] = "";
char last_id[24] = "";
while (!g_app_quit) {
usleep(300 * 1000);
int idx;
pthread_mutex_lock(&g_play.mu); idx = g_play.cur_song; pthread_mutex_unlock(&g_play.mu);
if (idx < 0 || idx >= g_st.song_count) {
char id[24];
pthread_mutex_lock(&g_play.mu);
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();
pthread_mutex_lock(&g_lyrmu);
g_lyr_song_id[0] = 0;
@@ -341,7 +366,6 @@ static void *lyric_worker(void *arg)
last_id[0] = 0;
continue;
}
const char *id = g_st.songs[idx].id;
if (!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" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C",
"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;
for (int i = 0; i < 6; 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);
g_st.songs[i].duration_ms = 180000 + i * 20000;
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);
g_st.playing_song = 0;
@@ -139,6 +143,8 @@ int main(int argc, char **argv)
g_play.cur_song = 0;
g_play.pos_ms = 94000;
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);
} else if (strcmp(name, "settings") == 0) {
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]);
g_st.songs[i].duration_ms = 180000 + i * 20000;
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.track_ids_count = 3;
} else {
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_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(不重叠) */
@@ -40,7 +41,16 @@ static ncm_rect_t g_lyr; /* 歌词区 */
static int cur_song(void)
{
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)
@@ -188,10 +198,10 @@ static void plr_draw_dyn(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 sc = cur_song();
const ncm_song_t *s = (sc >= 0) ? &g_st.songs[sc] : NULL;
const char *nm = s ? s->name : "暂无播放";
const char *ar = s ? s->art_name : "";
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);
@@ -204,7 +214,7 @@ static void plr_draw_dyn(void *ctx)
/* 进度条 + 时间 */
int pos = (int)player_manager_pos_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;
if (frac < 0) frac = 0; if (frac > 1000) frac = 1000;
char lp[16], rp[16];
@@ -302,11 +312,12 @@ void player_on_tap(int x, int y)
void player_play_song(int idx)
{
if (idx < 0 || idx >= g_st.song_count) return;
/* 二期:黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅),
* 无 al.picUrl(如无版权占位)时回落歌单封面。 */
const ncm_song_t *s = &g_st.songs[idx];
const char *cover = (s && s->al_pic[0]) ? s->al_pic : g_st.cover_url;
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);
@@ -338,13 +349,16 @@ void player_tick(uint64_t now)
if (sc != g_last_song) {
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); /* 歌名/歌手变 */
}
/* 元数据 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);
@@ -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,
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)
+74 -61
View File
@@ -25,7 +25,9 @@ static char g_title[256];
static volatile int g_load_err;
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_t g_plthr;
static int g_plthr_on;
@@ -33,8 +35,9 @@ static volatile int g_plgen;
static int g_placked;
static volatile int g_plloading;
static volatile int g_plloaded_idx;
static volatile int g_ploffset; /* 本页起始下标(0=首屏重头;>0=触底追加) */
static volatile int g_pl_more; /* 1=尚有下一页可加载(song_count < track_ids_count 且未到 NCM_SONG_MAX) */
static volatile int g_ens_lo, g_ens_hi; /* 滑窗补载目标区间(全局下标) */
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)
{
@@ -44,15 +47,24 @@ static void pl_drag_reset(void)
static int pl_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_st.song_count) return 0;
ncm_song_t *s = &g_st.songs[idx];
if (idx < 0 || idx >= g_st.track_ids_count) return 0;
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];
if (s->art_name[0] && s->album_name[0]) snprintf(sub, sizeof sub, "%s · %s", s->art_name, s->album_name);
else if (s->art_name[0]) snprintf(sub, sizeof sub, "%s", s->art_name);
else if (s->album_name[0]) snprintf(sub, sizeof sub, "%s", s->album_name);
else snprintf(sub, sizeof sub, "-");
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_s.art_name[0]) snprintf(sub, sizeof sub, "%s", s_s.art_name);
else if (s_s.album_name[0]) snprintf(sub, sizeof sub, "%s", s_s.album_name);
else snprintf(sub, sizeof sub, "-");
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = s->name;
out->title = s_s.name;
out->subtitle = sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0; out->enabled = 1;
@@ -64,12 +76,12 @@ static void pl_row_bg(void *ctx)
{
(void)ctx;
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();
int item = APP_ROW_H;
int off = meow_listview_scroll(g_list);
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++) {
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; /* 只画完整落在视口内的行 */
@@ -100,7 +112,7 @@ static void pl_draw(void *ctx)
int nmx = r_cov.x + r_cov.w + 12;
char meta[256];
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 字符合法边界逐字累加测宽)。 */
{
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);
/* 曲目行:红 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 off = meow_listview_scroll(g_list);
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++) {
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;
@@ -154,7 +166,7 @@ static void pl_draw_empty(void *ctx)
{
(void)ctx;
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_load_err == NCM_ERR_AUTH) 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;
if (meow_ui_activity_active() != ACT_PLAYLIST) return;
if (g_load_err != 0) return;
int total = (g_st.active_playlist >= 0 && g_st.active_playlist < g_st.playlist_count)
? g_st.playlists[g_st.active_playlist].track_count : g_st.song_count;
int total = g_st.track_ids_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];
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);
}
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(触底追加,防跳顶)。 */
/* apply:reset_scroll=1 时列表回顶并清选中(首屏/刷新);0=保持当前滚动(滑窗补载,防跳顶)。 */
static void pl_apply(int reset_scroll)
{
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 (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;
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;
if (g_host_mock) rc = 0;
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);
g_load_err = rc;
/* 本页后仍有多余 trackIds(tail 未到)且未到内存上限 → 还有下一页;否则到屏尾。 */
g_pl_more = (rc == NCM_OK) && (g_st.song_count < g_st.track_ids_count) &&
(g_st.song_count < NCM_SONG_MAX);
if (first) g_load_err = rc; /* 首屏失败 → 整页空态(点按重试) */
else g_ens_err = rc; /* 补载失败 → snack + 占位行保留(滚动/点按再试) */
g_plloading = 0;
g_plloaded_idx = my_idx;
g_plgen++;
@@ -210,19 +226,23 @@ static void *playlist_worker(void *arg)
return NULL;
}
/* 触底加载下一页:仅当仍有下一页、未在载、未失败、未到 NCM_SONG_MAX 时发。 */
static void pl_maybe_load_more(void)
/* 视口驱动滑窗补载:视口 ±24 行有未物化槽位 → 发 ensure worker(已在载/失败态/宿主 mock 不发)。 */
static void pl_maybe_ensure(void)
{
if (g_plthr_on || g_host_mock) return;
if (g_load_err != 0) return; /* 失败态:走点按重试,不自动循环 */
if (!g_pl_more) return;
if (g_st.song_count >= NCM_SONG_MAX) return;
/* 视口底接近列表底(最后一行即将可见即预取) */
int list_px = g_st.song_count * APP_ROW_H;
int max_scroll = list_px - r_list.h;
if (max_scroll < 0) max_scroll = 0;
if (meow_listview_scroll(g_list) < max_scroll - APP_ROW_H * 2) return;
g_ploffset = g_st.song_count; /* 下一页起始 = 已物化数 */
if (g_load_err != 0) return; /* 首屏失败态:走点按重试 */
int total = g_st.track_ids_count;
if (total <= 0) return;
int scroll = meow_listview_scroll(g_list);
int first = scroll / APP_ROW_H;
int last = (scroll + r_list.h) / APP_ROW_H + 1;
int lo = first - 24, hi = last + 24;
if (lo < 0) lo = 0;
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_plthr_on = 1;
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)
{
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;
g_load_err = 0;
g_st.song_count = 0;
g_st.track_ids_count = 0;
g_ploffset = 0;
g_pl_more = 0;
g_ens_err = 0;
g_ens_first = 1; /* 首屏:重建索引 + 物化首段 */
g_plloading = 1;
g_plthr_on = 1;
if (pthread_create(&g_plthr, NULL, playlist_worker, NULL) != 0) {
@@ -257,20 +275,15 @@ void playlist_tick(uint64_t now)
g_plloading = 0;
if (g_plloaded_idx != g_st.active_playlist) { playlist_refresh(); return; }
if (g_load_err != 0) {
if (g_ploffset == 0) {
g_st.song_count = 0; /* 首屏失败 → 整页空态(点按重试) */
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);
}
g_st.track_ids_count = 0; /* 首屏失败 → 整页空态(点按重试) */
pl_apply(1);
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)
@@ -307,7 +320,7 @@ int playlist_on_tap(int x, int y)
playlist_refresh();
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)) {
/* 误触修复:小拖动(<SWIPE_THRESH)会先滚动列表、release 又被 SDK 判成 TAP,旧的
* 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);
pl_drag_reset();
if (row < 0 || was_drag) return 1;
player_play_song(row);
player_play_song(row); /* 占位行也可点:播放线程经 appstate_song_fetch 物化该曲 */
return 1;
}
return 0;
@@ -326,7 +339,7 @@ int playlist_on_tap(int x, int y)
void playlist_touch_down(int x, int y)
{
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)) {
int row = meow_listview_hit(g_list, x, y);
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);
if (meow_listview_scroll(g_list) != before) {
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)
{
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 before = meow_listview_scroll(g_list);
meow_listview_scroll_by(g_list, del);
if (meow_listview_scroll(g_list) != before) {
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();
}
/* 后台 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)
{
(void)arg;
@@ -104,12 +106,12 @@ static void *search_worker(void *arg)
g_st.songs, NCM_SEARCH_MAX, &n);
pthread_mutex_lock(&g_mu);
if (rc == NCM_OK) {
appstate_adopt_songs(n); /* 收编 songs[0..n) 为窗口(写侧在锁内) */
g_st.search_count = n;
g_st.song_count = n; /* 结果集作为当前播放上下文 */
g_st.search_err = 0;
} else {
appstate_adopt_songs(0);
g_st.search_count = 0;
g_st.song_count = 0;
g_st.search_err = rc;
}
g_loading = 0;