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;
}