fix(ncm): 歌单切换两bug — ①暂停态跨歌单点歌旧信息(play同曲护栏改按曲目id判同曲而非裸队列下标,切歌单后同下标换歌被吞play→修play_is_same_song) ②切歌单封面残留(playlist_tick补cover_changed/ack+invalidate重画头块);附带占位行cover_request野id修复;宿主P15/covswitch PASS,热更ncm

This commit is contained in:
AskaEth committed 2026-09-04 00:14:37 +08:00
1 parent 62ec405dca
commit 6eccb17f9b
5 files changed
+150 -17

No files matched your search

+35 -9
View File
@@ -156,20 +156,46 @@ void player_manager_set_quality(int level, int cache_gb)
pthread_mutex_unlock(&g_play.mu); pthread_mutex_unlock(&g_play.mu);
} }
/* 同曲重入护栏的「同曲」判据(任务:跨歌单切换后,同一队列下标指向的是**另一首歌**)。
* 裸下标(song_idx==cur_song)在换歌单(播放队列内容变化)后不再成立——实测真机日志
* 「play: re-enter current idx=0 state=2 -> no restart」:A 停在 idx=0,切到 B 后点 idx=0
* 被当「点正在播的曲」吞掉 → 新歌永不起播、播放页停在上首+暂停;恢复播放(旧曲播完自动顺延
* 到不同下标)后才正常切换。故改以「候选曲 id == 正在播快照 id」判同曲:确证同曲才免重播
* 聚焦;候选未物化/队列已换(id 不符)→ 非确证,按新播放意图起播(正确方向)。
* 候选经 appstate_song_copy/appstate_search_song 做**廉价窗/数组拷贝**,绝不触网络
* (占位行未物化即返回 0,交下方正常起播)。不持 g_play.mu 调用(内部自锁读快照 id)。 */
static int play_is_same_song(int song_idx)
{
ncm_song_t cand;
int have = (appstate_get_queue() == NCM_QUEUE_SEARCH)
? (appstate_search_song(song_idx, &cand) == NCM_OK)
: (appstate_song_copy(song_idx, &cand) == NCM_OK);
if (!have || !cand.id[0]) return 0; /* 未物化:不能确证同曲 → 交下方正常起播 */
pthread_mutex_lock(&g_play.mu);
int same = g_play.play_song_valid && g_play.play_song.id[0] &&
strcmp(cand.id, g_play.play_song.id) == 0;
pthread_mutex_unlock(&g_play.mu);
return same;
}
void player_manager_play(int song_idx) void player_manager_play(int song_idx)
{ {
pthread_mutex_lock(&g_play.mu); pthread_mutex_lock(&g_play.mu);
/* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播: int is_cur = (song_idx == g_play.cur_song);
* 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。 int pending = g_pending_replay;
* 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。 int st = g_play.state;
* 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */ pthread_mutex_unlock(&g_play.mu);
if (song_idx == g_play.cur_song && !g_pending_replay && /* 任务3:点「正在播的曲」→ 只聚焦不重播;**但以曲目身份(id)而非裸下标判定**——跨歌单
(g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) { * 切换后同一队列下标已指向另一首歌(真实日志 `play: re-enter current idx=0` 误吞跨歌单
pthread_mutex_unlock(&g_play.mu); * 点歌),故在 PLAYING/PAUSED 且同下标时再校验候选曲 == 正在播快照:同曲才免重播,
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart", * 不同曲(队列已换)/未物化 → 照常起播。注意:set_quality 已置 g_pending_replay=1
song_idx, g_play.state); * (切音质后重播同曲须换档,P12),此护栏放行。非 PLAYING/PAUSED 不受影响,照旧起播。 */
if (is_cur && !pending && (st == NCM_PC_PLAYING || st == NCM_PC_PAUSED) &&
play_is_same_song(song_idx)) {
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d -> no restart", song_idx);
return; return;
} }
pthread_mutex_lock(&g_play.mu);
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */ g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
g_play.cur_song = song_idx; g_play.cur_song = song_idx;
g_play.cmd = 0; g_play.cmd = 0;
+59 -2
View File
@@ -9,6 +9,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> /* unlink(covswitch PPM 清理) */
ncm_app_state_t g_st; ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
@@ -83,6 +84,36 @@ static void mock_qr(void)
g_st.login_state = NCM_LOGIN_PENDING; g_st.login_state = NCM_LOGIN_PENDING;
} }
/* Bug1 回归(covswitch):写一张 8x8 纯色 24-bit BMP,供 cover_host_inject 注入两张不同封面
* (stb_image 明确支持 BMP;PPM 在宿主 stb 版本回 NULL → 注入失败)。 */
static int cov_make_bmp(const char *path, unsigned char r, unsigned char g, unsigned char b)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
int w = 8, h = 8, row = w * 3, img = row * h;
unsigned char hdr[54];
memset(hdr, 0, sizeof hdr);
hdr[0] = 'B'; hdr[1] = 'M';
uint32_t fsz = (uint32_t)(54 + img);
hdr[2] = (unsigned char)(fsz & 0xFF); hdr[3] = (unsigned char)((fsz >> 8) & 0xFF);
hdr[4] = (unsigned char)((fsz >> 16) & 0xFF); hdr[5] = (unsigned char)((fsz >> 24) & 0xFF);
hdr[10] = 54; /* 像素数据偏移 */
hdr[14] = 40; /* BITMAPINFOHEADER 大小 */
uint32_t ww = (uint32_t)w, hh = (uint32_t)h;
hdr[18] = (unsigned char)(ww & 0xFF); hdr[19] = (unsigned char)((ww >> 8) & 0xFF);
hdr[20] = (unsigned char)((ww >> 16) & 0xFF); hdr[21] = (unsigned char)((ww >> 24) & 0xFF);
hdr[22] = (unsigned char)(hh & 0xFF); hdr[23] = (unsigned char)((hh >> 8) & 0xFF);
hdr[24] = (unsigned char)((hh >> 16) & 0xFF); hdr[25] = (unsigned char)((hh >> 24) & 0xFF);
hdr[26] = 1; hdr[28] = 24; /* planes=1, bpp=24 */
hdr[34] = (unsigned char)(img & 0xFF); hdr[35] = (unsigned char)((img >> 8) & 0xFF);
hdr[36] = (unsigned char)((img >> 16) & 0xFF); hdr[37] = (unsigned char)((img >> 24) & 0xFF);
if (fwrite(hdr, 1, 54, f) != 54) { fclose(f); return -1; }
for (int y = 0; y < h; y++) /* 自底向上 BGR */
for (int x = 0; x < w; x++) { fputc(b, f); fputc(g, f); fputc(r, f); }
fclose(f);
return 0;
}
static int page_id(const char *name) static int page_id(const char *name)
{ {
if (strcmp(name, "login") == 0) return ACT_LOGIN; if (strcmp(name, "login") == 0) return ACT_LOGIN;
@@ -98,6 +129,7 @@ static int page_id(const char *name)
if (strcmp(name, "plsplay") == 0) return ACT_PLAYLIST; /* mini 栏目检(非播放页+播放态) */ if (strcmp(name, "plsplay") == 0) return ACT_PLAYLIST; /* mini 栏目检(非播放页+播放态) */
if (strcmp(name, "homeplay") == 0) return ACT_HOME; /* mini 栏目检(首页+播放态) */ if (strcmp(name, "homeplay") == 0) return ACT_HOME; /* mini 栏目检(首页+播放态) */
if (strcmp(name, "barhit") == 0) return ACT_PLAYLIST; /* mini 栏命中自检 */ if (strcmp(name, "barhit") == 0) return ACT_PLAYLIST; /* mini 栏命中自检 */
if (strcmp(name, "covswitch") == 0) return ACT_PLAYLIST;/* 歌单头块封面随全局封面刷新(Bug1) */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS; if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
if (strcmp(name, "search") == 0) return ACT_SEARCH; if (strcmp(name, "search") == 0) return ACT_SEARCH;
if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */ if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */
@@ -149,7 +181,8 @@ int main(int argc, char **argv)
g_st.login_state = NCM_LOGIN_PENDING; g_st.login_state = NCM_LOGIN_PENDING;
printf("live unikey=%s qr_url=%s size=%d\n", unikey, g_st.qr_url, sz); printf("live unikey=%s qr_url=%s size=%d\n", unikey, g_st.qr_url, sz);
} }
else if (strcmp(name, "playlist") == 0) { mock_playlists(); mock_songs(); } else if (strcmp(name, "playlist") == 0 || strcmp(name, "covswitch") == 0)
{ mock_playlists(); mock_songs(); }
else if (strcmp(name, "player") == 0 || strcmp(name, "plcover") == 0 || else if (strcmp(name, "player") == 0 || strcmp(name, "plcover") == 0 ||
strcmp(name, "pllyric") == 0 || strcmp(name, "gestest") == 0 || strcmp(name, "pllyric") == 0 || strcmp(name, "gestest") == 0 ||
strcmp(name, "grid") == 0) { strcmp(name, "grid") == 0) {
@@ -265,7 +298,7 @@ int main(int argc, char **argv)
if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create(); if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create();
else if (strcmp(name, "home") == 0) v = home_view_create(); else if (strcmp(name, "home") == 0) v = home_view_create();
else if (strcmp(name, "playlist") == 0 || strcmp(name, "plsplay") == 0 || else if (strcmp(name, "playlist") == 0 || strcmp(name, "plsplay") == 0 ||
strcmp(name, "barhit") == 0) v = playlist_view_create(); strcmp(name, "barhit") == 0 || strcmp(name, "covswitch") == 0) v = playlist_view_create();
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr/modetest */ else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr/modetest */
else if (page_id(name) == ACT_QUEUE) v = queue_view_create(); /* §NEW 播放队列页 */ else if (page_id(name) == ACT_QUEUE) v = queue_view_create(); /* §NEW 播放队列页 */
else if (strcmp(name, "settings") == 0) v = settings_view_create(); else if (strcmp(name, "settings") == 0) v = settings_view_create();
@@ -314,6 +347,30 @@ int main(int argc, char **argv)
if (page_id(name) == ACT_PLAYER) { if (page_id(name) == ACT_PLAYER) {
player_tick(meow_ui_time_ms()); player_tick(meow_ui_time_ms());
} }
if (strcmp(name, "covswitch") == 0) {
/* Bug1 回归:歌单页活跃,playlist_tick 须消费 cover_changed(头块封面随全局封面刷新)。
* 修复前 playlist_tick 不判 cover_changed/ack → 注入后 cover_changed 恒 1(头块永不重画,
* 停留上一张);修复后 tick 判变并 ack → 归 0(下次换封面再变 1,再 ack → 收敛到新封面)。 */
int ok = (cov_make_bmp("/tmp/ncm_cov_a.bmp", 220, 40, 40) == 0 &&
cov_make_bmp("/tmp/ncm_cov_b.bmp", 40, 90, 220) == 0);
if (ok) {
cover_host_inject("/tmp/ncm_cov_a.bmp");
int c1 = cover_changed(); /* 注入后:新封面待绘制(gen>acked) */
playlist_tick(0); /* 修复点:消费 */
int c2 = cover_changed(); /* 应 ack 归 0 */
cover_host_inject("/tmp/ncm_cov_b.bmp");
int c3 = cover_changed(); /* 再注入:又待绘制 */
playlist_tick(0);
int c4 = cover_changed(); /* 再 ack 归 0 */
ok = (c1 && !c2 && c3 && !c4);
printf("covswitch: A->changed=%d tick->acked=%d B->changed=%d tickB->acked=%d -> %s\n",
c1, c2, c3, c4, ok ? "PASS" : "FAIL");
} else {
printf("covswitch: init FAIL (bmp write)\n");
}
if (!ok) return 1;
unlink("/tmp/ncm_cov_a.bmp"); unlink("/tmp/ncm_cov_b.bmp");
}
if (strcmp(name, "search") == 0 || strcmp(name, "searchtap") == 0) { if (strcmp(name, "search") == 0 || strcmp(name, "searchtap") == 0) {
search_submit(g_st.search_query); /* 宿主:应用预置搜索结果 */ search_submit(g_st.search_query); /* 宿主:应用预置搜索结果 */
} }
+6 -4
View File
@@ -743,13 +743,15 @@ void player_play_song(int idx)
g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:歌单窗口队列 */ g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:歌单窗口队列 */
/* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅); /* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅);
* 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。 * 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§17)。
* §18:g_st.cover_url 只作「歌单封面兜底」。 */ * §18:g_st.cover_url 只作「歌单封面兜底」。占位行直点(appstate_song_copy 失败)时
* s 未初始化,**不能**把 s.id 传给 cover_request(会带出一段野曲 id 参与兜底判冷却)——
* 仅物化成功才带曲目 id,否则 NULL(歌单封面,不带兜底)。 */
ncm_song_t s; ncm_song_t s;
const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0]) int have_s = (appstate_song_copy(idx, &s) == 0);
? s.al_pic : g_st.cover_url; const char *cover = (have_s && s.al_pic[0]) ? s.al_pic : g_st.cover_url;
player_manager_play(idx); player_manager_play(idx);
appstate_set_playing_song(idx, NULL); appstate_set_playing_song(idx, NULL);
cover_request(cover, s.id); /* 曲目专辑封面:带 id,死链时 song/detail 兜底(REPORT §NEW 任务1) */ cover_request(cover, have_s ? s.id : NULL);
g_ppage = 0; /* 新曲 → 回封面页 */ g_ppage = 0; /* 新曲 → 回封面页 */
meow_view_page_push(ACT_PLAYER); meow_view_page_push(ACT_PLAYER);
} }
+8
View File
@@ -386,6 +386,14 @@ void playlist_refresh(void)
void playlist_tick(uint64_t now) void playlist_tick(uint64_t now)
{ {
(void)now; (void)now;
/* 歌单头块封面跨页同步:全局单封面(与播放页/mini 栏共享)由 cover worker 异步换入,
* 本页须判 cover_changed 并重画头块封面带——否则切歌单后(或从播放页回退时)头块永远
* 停留上一张(歌单封面不随切换刷新,实机 Bug1:切到 B 顶图仍是 A 的)。仅在播放页 tick
* 消费 cover_ack 是不够的(该页 inactive 不跑);此处 play 页活跃时消费并失效封面带。 */
if (cover_changed()) {
cover_ack();
meow_ui_invalidate(r_cov.x, r_cov.y, r_cov.w, r_cov.h);
}
if (!g_plthr_on) return; if (!g_plthr_on) return;
if (g_plgen == g_placked) return; if (g_plgen == g_placked) return;
g_placked = g_plgen; g_placked = g_plgen;
+42 -2
View File
@@ -16,6 +16,9 @@
* 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。 * 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。
* P13 陈旧 hires.mp3(flac 门禁前静默降级产物,hires 键下存 mp3):song_local_file * P13 陈旧 hires.mp3(flac 门禁前静默降级产物,hires 键下存 mp3):song_local_file
* 迁移重命名到 exhigh 键(song_<id>_exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。 * 迁移重命名到 exhigh 键(song_<id>_exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。
* P14 歌单里点「正在播的同曲」→ 只聚焦不重播(任务3 re-entry,同曲 id 判据)。
* P15 跨歌单切换后同一队列下标点歌 → 不被同曲护栏吞,起播(Bug2 回归:真机日志
* `play: re-enter current idx=0 state=2 -> no restart`,新歌永不播)。
* *
* 编译(宿主,ncm_app 目录下): * 编译(宿主,ncm_app 目录下):
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \ * gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
@@ -61,13 +64,16 @@ void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (voi
/* core/ncm_common.c 提供 ncm_now_ms(墙钟毫秒);本 sim 不链 ncm_common.o,提供同语义桩。 */ /* core/ncm_common.c 提供 ncm_now_ms(墙钟毫秒);本 sim 不链 ncm_common.o,提供同语义桩。 */
int64_t ncm_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000; } int64_t ncm_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000; }
/* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */ /* ---- appstate stub:窗内全命中,id=g_song_base+1000+idx;fee 可注入(§20 P8)----
* g_song_base 模拟「播放队列内容变化」:P14 跨歌单切换后,同一队列下标指向另一首歌
* (base 0→5000,idx=0 由 id=1000 变 id=6000),用于回归 Bug2 的同曲重入护栏。 */
static int g_song_base = 0;
static int g_fee = 0; static int g_fee = 0;
int appstate_song_copy(int gidx, ncm_song_t *out) int appstate_song_copy(int gidx, ncm_song_t *out)
{ {
if (gidx < 0 || gidx >= g_st.track_ids_count) return -1; if (gidx < 0 || gidx >= g_st.track_ids_count) return -1;
memset(out, 0, sizeof *out); memset(out, 0, sizeof *out);
snprintf(out->id, sizeof out->id, "%d", 1000 + gidx); snprintf(out->id, sizeof out->id, "%d", g_song_base + 1000 + gidx);
snprintf(out->name, sizeof out->name, "Song%d", gidx); snprintf(out->name, sizeof out->name, "Song%d", gidx);
out->duration_ms = 180000; out->duration_ms = 180000;
out->fee = g_fee; out->fee = g_fee;
@@ -77,6 +83,10 @@ int appstate_song_fetch(int gidx, ncm_song_t *out) { return appstate_song_copy(g
/* §23:player_manager 改走 appstate_queue_*(队列来源)——sim 恒 PLAYLAST 队列 = 窗口 */ /* §23:player_manager 改走 appstate_queue_*(队列来源)——sim 恒 PLAYLAST 队列 = 窗口 */
int appstate_queue_count(void) { return g_st.track_ids_count; } int appstate_queue_count(void) { return g_st.track_ids_count; }
int appstate_queue_song(int idx, ncm_song_t *out) { return appstate_song_copy(idx, out); } int appstate_queue_song(int idx, ncm_song_t *out) { return appstate_song_copy(idx, out); }
/* P14 相关:player_manager_play 的同曲护栏(play_is_same_song)会读队列来源 + 搜索数组;
* sim 恒歌单窗口队列、搜索数组为空。appstate.c 未链入,两符号此处提供桩。 */
int appstate_get_queue(void) { return NCM_QUEUE_PLAYLIST; }
int appstate_search_song(int i, ncm_song_t *out) { (void)i; (void)out; return NCM_ERR_ARG; }
/* ---- 缓存/网络 stub ---- /* ---- 缓存/网络 stub ----
* g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载); * g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载);
@@ -650,6 +660,36 @@ int main(void)
usleep(200 * 1000); usleep(200 * 1000);
player_manager_stop_thread(); player_manager_stop_thread();
printf("== P15: 跨歌单切换后同 idx 点歌不被「同曲只聚焦」吞(Bug2 回归)==\n");
/* 线程已停 → 纯逻辑判护栏(无竞态)。手工造「歌单 A 的歌停在 idx=0(id=1000)」的
* 播放快照态;然后模拟切到歌单 B(g_song_base 0→5000:同一队列下标 idx=0 现指向 id=6000
* 的另一首歌——真机日志实锤 `play: re-enter current idx=0 state=2 -> no restart`:新歌
* 永不起播、播放页停在上首+暂停)。护栏改按曲目 id 判同曲后,跨歌单同下标点歌必须起播。 */
pthread_mutex_lock(&g_play.mu);
g_play.cur_song = 0;
g_play.state = NCM_PC_PAUSED;
g_play.play_song_valid = 1;
memset(&g_play.play_song, 0, sizeof g_play.play_song);
snprintf(g_play.play_song.id, sizeof g_play.play_song.id, "1000");
g_play.seq = 90; g_play.pos_ms = 1000; g_play.err = 0;
g_play.dl_done = 0; g_play.dl_total = 0;
pthread_mutex_unlock(&g_play.mu);
int seq_before = g_play.seq; /* 线程已停,无锁读安全 */
/* (a) 同歌单点「正在播的同曲」(同下标、同 id)→ 护栏放行:确证同曲 → 只聚焦不重播 */
g_song_base = 0;
player_manager_play(0);
CHECK(player_manager_state() == NCM_PC_PAUSED && player_manager_playing_song() == 0 &&
g_play.seq == seq_before,
"same-playlist re-tap -> PAUSED/cur=0/seq=%d (expect 不重播)", g_play.seq);
/* (b) 切到歌单 B:队列内容变化(idx=0 现为 id=6000 的另一首)→ 同 idx 点歌必须起播 */
g_song_base = 5000;
player_manager_play(0);
CHECK(player_manager_state() == NCM_PC_BUFFER && g_play.seq == seq_before + 1,
"cross-playlist idx=0 tap -> BUFFER/seq=%d (expect 起播,不被吞)", g_play.seq);
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n"); printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
return g_fail; return g_fail;
} }