From 6eccb17f9b994109c23f355179128775b5789f75 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 4 Sep 2026 00:14:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(ncm):=20=E6=AD=8C=E5=8D=95=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E4=B8=A4bug=20=E2=80=94=20=E2=91=A0=E6=9A=82=E5=81=9C?= =?UTF-8?q?=E6=80=81=E8=B7=A8=E6=AD=8C=E5=8D=95=E7=82=B9=E6=AD=8C=E6=97=A7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF(play=E5=90=8C=E6=9B=B2=E6=8A=A4=E6=A0=8F?= =?UTF-8?q?=E6=94=B9=E6=8C=89=E6=9B=B2=E7=9B=AEid=E5=88=A4=E5=90=8C?= =?UTF-8?q?=E6=9B=B2=E8=80=8C=E9=9D=9E=E8=A3=B8=E9=98=9F=E5=88=97=E4=B8=8B?= =?UTF-8?q?=E6=A0=87,=E5=88=87=E6=AD=8C=E5=8D=95=E5=90=8E=E5=90=8C?= =?UTF-8?q?=E4=B8=8B=E6=A0=87=E6=8D=A2=E6=AD=8C=E8=A2=AB=E5=90=9Eplay?= =?UTF-8?q?=E2=86=92=E4=BF=AEplay=5Fis=5Fsame=5Fsong)=20=E2=91=A1=E5=88=87?= =?UTF-8?q?=E6=AD=8C=E5=8D=95=E5=B0=81=E9=9D=A2=E6=AE=8B=E7=95=99(playlist?= =?UTF-8?q?=5Ftick=E8=A1=A5cover=5Fchanged/ack+invalidate=E9=87=8D?= =?UTF-8?q?=E7=94=BB=E5=A4=B4=E5=9D=97);=E9=99=84=E5=B8=A6=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E8=A1=8Ccover=5Frequest=E9=87=8Eid=E4=BF=AE=E5=A4=8D;?= =?UTF-8?q?=E5=AE=BF=E4=B8=BBP15/covswitch=20PASS,=E7=83=AD=E6=9B=B4ncm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch_v2/ncm_app/app/play/player_manager.c | 44 ++++++++++++---- patch_v2/ncm_app/app/test/test_ncm_app.c | 61 +++++++++++++++++++++- patch_v2/ncm_app/app/ui/player.c | 10 ++-- patch_v2/ncm_app/app/ui/playlist.c | 8 +++ patch_v2/ncm_app/bench/player_flow_sim.c | 44 +++++++++++++++- 5 files changed, 150 insertions(+), 17 deletions(-) diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index e1aae85..33c4d12 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -156,20 +156,46 @@ void player_manager_set_quality(int level, int cache_gb) 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) { pthread_mutex_lock(&g_play.mu); - /* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播: - * 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。 - * 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。 - * 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */ - if (song_idx == g_play.cur_song && !g_pending_replay && - (g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) { - pthread_mutex_unlock(&g_play.mu); - meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart", - song_idx, g_play.state); + int is_cur = (song_idx == g_play.cur_song); + int pending = g_pending_replay; + int st = g_play.state; + pthread_mutex_unlock(&g_play.mu); + /* 任务3:点「正在播的曲」→ 只聚焦不重播;**但以曲目身份(id)而非裸下标判定**——跨歌单 + * 切换后同一队列下标已指向另一首歌(真实日志 `play: re-enter current idx=0` 误吞跨歌单 + * 点歌),故在 PLAYING/PAUSED 且同下标时再校验候选曲 == 正在播快照:同曲才免重播, + * 不同曲(队列已换)/未物化 → 照常起播。注意:set_quality 已置 g_pending_replay=1 + * (切音质后重播同曲须换档,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; } + pthread_mutex_lock(&g_play.mu); g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */ g_play.cur_song = song_idx; g_play.cmd = 0; diff --git a/patch_v2/ncm_app/app/test/test_ncm_app.c b/patch_v2/ncm_app/app/test/test_ncm_app.c index 94509ff..e031076 100644 --- a/patch_v2/ncm_app/app/test/test_ncm_app.c +++ b/patch_v2/ncm_app/app/test/test_ncm_app.c @@ -9,6 +9,7 @@ #include #include #include +#include /* unlink(covswitch PPM 清理) */ ncm_app_state_t g_st; 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; } +/* 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) { 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, "homeplay") == 0) return ACT_HOME; /* 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, "search") == 0) return ACT_SEARCH; 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; 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 || strcmp(name, "pllyric") == 0 || strcmp(name, "gestest") == 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(); else if (strcmp(name, "home") == 0) v = home_view_create(); 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_QUEUE) v = queue_view_create(); /* §NEW 播放队列页 */ 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) { 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) { search_submit(g_st.search_query); /* 宿主:应用预置搜索结果 */ } diff --git a/patch_v2/ncm_app/app/ui/player.c b/patch_v2/ncm_app/app/ui/player.c index aff901f..d513235 100644 --- a/patch_v2/ncm_app/app/ui/player.c +++ b/patch_v2/ncm_app/app/ui/player.c @@ -743,13 +743,15 @@ void player_play_song(int idx) g_st.queue_src = NCM_QUEUE_PLAYLIST; /* §23:歌单窗口队列 */ /* 黑胶封面用真实专辑图(song_detail 的 al.picUrl,经 ncm_cache 缓存/限幅); * 滑窗未物化(占位行直点)时回落歌单封面,元数据由播放线程物化后快照接管(§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; - const char *cover = (appstate_song_copy(idx, &s) == 0 && s.al_pic[0]) - ? s.al_pic : g_st.cover_url; + int have_s = (appstate_song_copy(idx, &s) == 0); + const char *cover = (have_s && s.al_pic[0]) ? s.al_pic : g_st.cover_url; player_manager_play(idx); 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; /* 新曲 → 回封面页 */ meow_view_page_push(ACT_PLAYER); } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index dea9605..f786194 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -386,6 +386,14 @@ void playlist_refresh(void) void playlist_tick(uint64_t 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_plgen == g_placked) return; g_placked = g_plgen; diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index db5bee3..0de5cd3 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -16,6 +16,9 @@ * 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。 * P13 陈旧 hires.mp3(flac 门禁前静默降级产物,hires 键下存 mp3):song_local_file * 迁移重命名到 exhigh 键(song__exhigh.mp3)+ 本档视为 miss 走下载(§NEW 任务2)。 + * P14 歌单里点「正在播的同曲」→ 只聚焦不重播(任务3 re-entry,同曲 id 判据)。 + * P15 跨歌单切换后同一队列下标点歌 → 不被同曲护栏吞,起播(Bug2 回归:真机日志 + * `play: re-enter current idx=0 state=2 -> no restart`,新歌永不播)。 * * 编译(宿主,ncm_app 目录下): * 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,提供同语义桩。 */ 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; int appstate_song_copy(int gidx, ncm_song_t *out) { if (gidx < 0 || gidx >= g_st.track_ids_count) return -1; 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); out->duration_ms = 180000; 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 队列 = 窗口 */ 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); } +/* 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 ---- * g_cache_ok=1 → lookup 命中(给真实存在的 /tmp 文件,免下载); @@ -650,6 +660,36 @@ int main(void) usleep(200 * 1000); 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"); return g_fail; }