/* play/player_manager.c — 播放线程:URL 获取(全档表逐级降级)→缓存(命中直接播/否则边下边播)→ * minimp3/dr_flac 解码(stream_src 回调)→audio_out 直写。UI 不卡(网络+解码全在此线程)。 * * 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的 * 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED); * seek=seek 解码器+重开输出到目标 ms(flac 精确帧/mp3 字节 offset 近似);下一/上一首=打断当前 run_song 返回外层。 * 所有跨线程共享状态在 g_play (mu)。g_st.ctx 的 device_id/cookie/cache 在播放期间稳定(登出会先停播)。 * 返回码/错误经 g_play.err/errmsg 供 UI 提示。 * §19:解码经 decoder.c 按 magic 分发(fLaC→dr_flac / 其它→minimp3);rate/ch/bits 按实际流 * 给 audio_out(flac ≤16bit→S16,>16bit→S32;audiod 格式归一/DAC 原生率跟随在服务端); * 音频缓存键 = 歌曲ID+音质档位(用户定案 NCM_CACHE_DESIGN §2.5),旧无档位键温和兼容。 * §20:音质体系全量——用户档超会员权限不硬拒(钳顶后逐级降级,eff_level 回填供播放页 * 「已降级至 xx」标注;下一首回到用户档);fee∈{1,4} 且非会员 → NCM_ERR_VIP 不发请求。 * §21:边下边播(NCM_STREAM_PLAY 方案 A)——下载拆到独立线程(write(2) 直写 .tmp + * stream_src 推进可读边界),解码经 ncm_dec_open_src 的 read_cb(严格 fread 语义,F7) * pread 同一文件;预缓冲 ≥128KB 才 open(minimp3 MINIMP3_IO_SIZE 强制门槛,F3); * 下载完成由播放线程 commit(rename);缓存命中用 final=size 的静态 stream_src(P1: * 不再 mmap 整文件);http 30s 绝对 deadline 已改空闲超时(P2)。.part 续传/LRU 防淘汰 * 留阶段 3。 * §25:切音质后重播同一曲——同 idx 再 play 时 cur/state 都不变(cur 同、state 由 PLAYING * 置 BUFFER),解码主循环此前只认 cur!=idx / STOPPED,导致: * ①旧曲(旧档)继续出声而 state 停在 BUFFER(UI 恒「缓冲中」);②旧曲收尾后外层把 * BUFFER 当「自然播完」顺延下一首,新档位永不生效。修法:以 seq 标识 play intent * (still_intent 取代仍_current),decode 主循环 seq 变化即 break,外层「自然播完」仅指 * state==PLAYING——同 idx 新 intent 回到外层重跑 run_song(换档重播)。 */ #include "../ncm_app_internal.h" #include #include #include #include #include #include "decoder.h" #include "stream_src.h" #include "mem_watch.h" static pthread_t g_thr; static int g_thr_on; static int g_want_run = 1; /* 线程运行标志(quit=0) */ #define PCM_CHUNK_FRAMES 4096 /* 每块帧数(≈旧 8192 样本/双声道) */ /* ---- 线程内部一次性歌的状态 ---- */ static char g_path[512]; /* 本地音频路径(缓存,mp3/flac) */ static int64_t g_file_bytes; static int g_level; /* 音质档(来自设置) */ static int g_cache_gb; /* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档 * (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的 * 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */ static int g_pending_replay; /* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁), * 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */ static void on_audio_preempt(const char *by_app, void *ctx) { (void)ctx; meow_log(MEOW_LOG_WARN, "play: preempted by [%s] -> pause", (by_app && by_app[0]) ? by_app : "?"); player_manager_pause(1); } int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; } int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; } int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; } 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; } /* 实际生效音质档(§20):下载时由 song_url_auto 回填;缓存命中按 ext 推断;-1=未定。 */ int player_manager_eff_level(void) { int e; pthread_mutex_lock(&g_play.mu); e = g_play.eff_level; pthread_mutex_unlock(&g_play.mu); return e; } /* 本曲下载进度(UI 缓冲反馈,§18)。total=0 表示未知(song_url 未给 size / 缓存命中)。 */ void player_manager_dl(int64_t *done, int64_t *total) { pthread_mutex_lock(&g_play.mu); if (done) *done = g_play.dl_done; if (total) *total = g_play.dl_total; pthread_mutex_unlock(&g_play.mu); } /* 播放曲快照(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; g_cache_gb = cache_gb; pthread_mutex_lock(&g_play.mu); g_play.err = 0; ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb); g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */ pthread_mutex_unlock(&g_play.mu); } 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); return; } g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */ g_play.cur_song = song_idx; g_play.cmd = 0; g_play.state = NCM_PC_BUFFER; g_play.pos_ms = 0; g_play.err = 0; /* 新播放意图清旧错误(UI 错误条随新点歌消失) */ g_play.dl_done = 0; g_play.dl_total = 0; g_play.seq++; g_play.eff_level = -1; /* 新播放意图:生效档未定(song_local_file 回填) */ g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */ g_play.underrun_ms = 0; g_play.stalled = 0; g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */ pthread_mutex_unlock(&g_play.mu); } void player_manager_pause(int on) { pthread_mutex_lock(&g_play.mu); if (on) { if (g_play.state == NCM_PC_PLAYING) g_play.state = NCM_PC_PAUSED; } else { if (g_play.state == NCM_PC_PAUSED) g_play.state = NCM_PC_PLAYING; } pthread_mutex_unlock(&g_play.mu); } void player_manager_seek(int64_t ms) { pthread_mutex_lock(&g_play.mu); g_play.cmd = NCM_CTL_SEEK; g_play.cmd_seek_ms = ms; pthread_mutex_unlock(&g_play.mu); } void player_manager_next(int dir) { pthread_mutex_lock(&g_play.mu); int c = g_play.cur_song + dir; if (c < 0) c = 0; int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */ if (c >= cnt) c = cnt - 1; if (c != g_play.cur_song) { g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */ g_play.cur_song = c; g_play.cmd = 0; g_play.state = NCM_PC_BUFFER; g_play.pos_ms = 0; g_play.err = 0; g_play.dl_done = 0; g_play.dl_total = 0; g_play.seq++; g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */ g_play.underruns = 0; g_play.underrun_ms = 0; g_play.stalled = 0; g_play.refaults = 0; } pthread_mutex_unlock(&g_play.mu); } void player_manager_stop(void) { pthread_mutex_lock(&g_play.mu); g_pending_replay = 0; /* 停止:清「切档重播」待办 */ 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); } /* ---- 每首歌的播放体(在外层循环内调用)---- */ /* 边下边播每首曲的源对象(§21/阶段3):读端 stream_src + 下载路径写端/线程。 * run_song 栈上分配,下载线程 join 后才返回,生命周期安全。 */ #define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */ #define STREAM_RETRY_MAX 1 /* 阶段3(§21.8/§4.1):首开声前失败重试上限(1 次,防风控) */ #define STREAM_RETRY_BACKOFF_MS 800 /* 重试退避:避免连环请求轰炸(真实 NCM 节制,§7 风控) */ /* song_download_attempt 返回哨兵(与 NCM_ERR_* 不冲突) */ #define DL_RETRYABLE (-1001) /* 预缓冲前网络失败:可整首重试 */ #define DL_ABORTED (-1002) /* 用户切歌/退出:静默放弃,不重试 */ typedef struct { stream_src_t *src; /* 读端(下载路径 written 由下载线程推进;缓存命中=静态源) */ ncm_cache_writer_t *w; /* 下载路径写端(commit/abort 归播放线程);缓存命中=NULL */ pthread_t dl_thr; int dl_thr_on; int dl_rc; /* 下载线程结果(NCM_OK / 负错误码) */ char url[1024];/* 下载线程用(song_local_file 栈上 u.url 生命周期不够,拷贝) */ size_t maxb; /* 下载限幅 */ int64_t total; /* 标称大小(song_url size;0=未知;mp3 seek 分母) */ /* 阶段3 .part 续传(§5.2) */ uint64_t resume_base; /* >0=断点续传起点(.part 大小);Http 带 Range: bytes=- */ int first_body; /* 首个 body 回调未处理(按响应状态切换写姿态) */ int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */ int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */ uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */ /* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */ char gov_path[512]; /* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */ char gov_final_path[512]; } song_src_t; /* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针 * (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */ static uint64_t gov_read_pos(void *ctx) { return stream_src_read_pos((stream_src_t *)ctx); } /* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 + * 锁内推进 written + broadcast 唤醒读端 + 进度反馈。 * 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态—— * 206(服务器支持 Range):正常追加到 .part 尾; * 200(服务器忽略 Range):body 头 resume_base 字节与 .part 前缀重复 → skip 丢弃; * 416/其它:放弃续传(need_fresh=1,abort),交调用方删残体整首重下。 */ static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len) { song_src_t *ss = (song_src_t *)ctx; if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */ /* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停 * OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程 * (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */ while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src)) usleep(50 * 1000); if (ss->first_body) { ss->first_body = 0; int st = ss->st_ptr ? *ss->st_ptr : 0; if (ss->resume_base > 0 && st != 206 && st != 200) { ss->need_fresh = 1; /* 416/其它:放弃续传 */ return 1; /* abort http(error body 不入盘) */ } if (ss->resume_base > 0 && st == 200) ss->skip = ss->resume_base; /* 全量头段与 .part 重复 */ } if (ss->skip) { /* 丢弃重复前缀字节 */ if (ss->skip >= len) { ss->skip -= len; return 0; } data += (size_t)ss->skip; len -= (size_t)ss->skip; ss->skip = 0; } if (ncm_cache_write(ss->w, data, len) != NCM_OK) return 1; stream_src_note_written(ss->src, (uint64_t)len); pthread_mutex_lock(&g_play.mu); g_play.dl_done += (int64_t)len; pthread_mutex_unlock(&g_play.mu); return 0; } static void *dl_thread(void *arg) { song_src_t *ss = (song_src_t *)arg; ncm_http_req_t req; memset(&req, 0, sizeof req); req.url = ss->url; req.method = "GET"; /* 续传:Range 头(§5.2)。URL 为本次新拉签名(不复用旧 URL,风控不增请求)。 * UA 现状保留;Connection: close 由 http 层补。 */ char hdrs[1200]; if (ss->resume_base > 0) snprintf(hdrs, sizeof hdrs, "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\n" "Range: bytes=%llu-", (unsigned long long)ss->resume_base); else snprintf(hdrs, sizeof hdrs, "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"); req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; /* 绝对段(连接/握手/头)上限;body 空闲超时 15s(P2) */ /* 状态感知下载:首 body 前写入 *status(续传分支)。同线程,st_ptr 无竞态。 */ int status = 0; ss->st_ptr = &status; ss->first_body = 1; ncm_http_resp_t resp; int rc = ncm_http_download_status(&req, stream_dl_cb, ss, ss->maxb, &resp, &status); ss->st_ptr = NULL; if (rc == NCM_OK) { /* 放行 200 与(续传时的)206;其余按 HTTP 错。续传遇 200=服务器忽略 Range(skip 已丢头段)。 */ int ok_st = (ss->resume_base > 0) ? (resp.status == 206 || resp.status == 200) : (resp.status == 200); if (!ok_st) rc = NCM_ERR_HTTP; } ss->dl_rc = rc; /* 结束通知:成功 final=全长;失败/中止 final=截断点+failed(读端播到截断点自然结束,F5) */ stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc); return NULL; } /* 收尾(播放线程,解码循环退出后调用):中止在途下载并 join → commit(rename 转正) 或 * abort(阶段3:中断保留 .part 供下次续传;拒绝续传的 416 删残体)。 */ static void song_src_dispose(song_src_t *ss) { if (ss->dl_thr_on) { stream_src_abort(ss->src); /* 已完成则无副作用;在途则 cb 返回 NZ 中止 */ pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; } if (ss->w) { if (ss->dl_rc == NCM_OK) { /* 下载完整:commit 转正进 LRU 池(rename 不影响读端已开 fd,此处已近曲终) */ int64_t fin = (int64_t)stream_src_final(ss->src); meow_log(MEOW_LOG_INFO, "play: dl done id? final=%lld commit", (long long)fin); if (ncm_cache_commit(ss->w) == 0) ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */ } else if (ss->need_fresh) { /* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */ meow_log(MEOW_LOG_WARN, "play: dl 416/refuse resume -> drop .part (dl_rc=%d)", ss->dl_rc); ncm_cache_abort(ss->w); } else { /* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */ meow_log(MEOW_LOG_INFO, "play: dl interrupt dl_rc=%d kept bytes=%lld -> .part", ss->dl_rc, (long long)stream_src_written(ss->src)); ncm_cache_abort_part(ss->w); } ss->w = NULL; } if (ss->src) { stream_src_close(ss->src); ss->src = NULL; } } /* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做 * L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */ static void song_gov_cleanup(song_src_t *ss) { if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path); if (ss->dl_rc == NCM_OK && ss->gov_final_path[0]) meow_cachegov_drop_finished(ss->gov_final_path); } /* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song__."。 * 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。 * §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */ static void audio_cache_key(char *out, size_t sz, const char *id, int level) { snprintf(out, sz, "%s_%s", id, ncm_api_level_key(level)); } /* §档位保真(REPORT §NEW 任务2②):显示档以响应实际格式/码率为准—— * - type=flac:采信降级链 eff(无损档链已保证 flac,br/采样由解码后定)。 * - type=mp3:绝不显示 Hi-Res 及以上(Hi-Res 需 >16bit/高采样,mp3 达不到)。按响应 br * 参与判定:≤128k→标准;否则→极高 HQ(≤320k,仅降级链中间档「较高 192k」不进 UI,不上屏)。 * - br 未知:钳 chain_eff 到 ≤EXHIGH(保守不夸大)。 * 返回 UI 显示用档位(g_play.eff_level)。 */ static int eff_level_for_resp(const ncm_play_url_t *u, int chain_eff, int want) { if (u && ncm_streq(u->type, "flac")) return (chain_eff >= 0) ? chain_eff : want; int br = u ? u->br : 0; if (br > 0) return (br <= 128000) ? NCM_LEVEL_STANDARD : NCM_LEVEL_EXHIGH; /* 按实际码率 */ int e = (chain_eff >= 0) ? chain_eff : want; return (e < NCM_LEVEL_EXHIGH) ? e : NCM_LEVEL_EXHIGH; /* 未知:钳到 ≤EXHIGH */ } /* 同曲重播护栏(§25 切音质后重播同一曲,替代 §18 的 still_current):仅 cur/state 不够—— * 切音质时 play_manager_play * 用的 idx 与在播曲相同(cur 不变)且置 state=BUFFER,仍会被 still_current 当「我的 intent * 有效」而漏判。play/next 都会 seq++(新播放意图),故以 seq 判别「本次 intent 是否仍有效」。 * 调用方需持有 run_song 开始时捕获的 my_seq(本曲 intent 的 seq)。 */ static int still_intent(int idx, int my_seq) { pthread_mutex_lock(&g_play.mu); int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER) && (g_play.seq == my_seq); pthread_mutex_unlock(&g_play.mu); return ok; } /* 一次「取 URL → 开写端(命中 .part 则续传) → 起下载线程 → 等预缓冲 ≥128KB」尝试(阶段3)。 * 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试); * DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。 * *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */ static int song_download_attempt(const ncm_song_t *s, int idx, int seq, const char *key, int want, song_src_t *ss, char *path, size_t psz, int64_t *bytes, int *real_err) { const char *id = s->id; ss->resume_base = 0; ss->skip = 0; ss->need_fresh = 0; ss->first_body = 1; /* 每次尝试干净 */ /* 播放地址:账户/fee 感知降级(auto_av 链内钳顶+门槛跳档+fee 门禁,300 修复) */ ncm_play_url_t u; int eff = -1; int rc = ncm_api_song_url_auto_av(g_st.ctx.device_id, g_st.ctx.cookie, id, account_vip(), s->fee, want, &u, &eff); if (rc != NCM_OK) return rc; if (!u.url[0]) return NCM_ERR_NOTFOUND; pthread_mutex_lock(&g_play.mu); g_play.eff_level = eff_level_for_resp(&u, eff, want); /* §2②:显示档按实际格式/码率 */ g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */ pthread_mutex_unlock(&g_play.mu); /* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */ const char *ext = "mp3"; if (ncm_streq(u.type, "flac")) ext = "flac"; else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac"; /* 写前检查:池大小超限先按 LRU 淘汰(半成品 .tmp/.part 已被 evict 跳过,无在途误删风险) */ ncm_cache_evict(&g_st.ctx.cache); /* 断点续传起点(§5.2):.part 残体存在且本次 URL 的 size > 残体大小 → 续传: * 追加打开 .part(O_APPEND,保留已写字节),written 从 part_size 起算(读端秒开播)。 */ uint64_t part_size = 0; int have_part = (ncm_cache_part_size(&g_st.ctx.cache, key, ext, &part_size) == NCM_OK) && part_size > 0; int resuming = 0; if (have_part && (long)part_size < u.size) { rc = ncm_cache_open_resume(&g_st.ctx.cache, key, ext, &ss->w); if (rc == NCM_OK) { ss->resume_base = part_size; resuming = 1; } else have_part = 0; /* 残体打开失败:转整首 */ } if (!resuming) { if (have_part) ncm_cache_part_remove(&g_st.ctx.cache, key, ext); /* 残体过期(size 不符):删 */ rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w); if (rc != NCM_OK) return rc; } if (stream_src_open_stream_at(&ss->src, ncm_cache_writer_tmp_path(ss->w), resuming ? part_size : 0) != 0) { ncm_cache_abort(ss->w); ss->w = NULL; return NCM_ERR_IO; } ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */ ss->total = (u.size > 0) ? u.size : 0; /* §NEW 任务1(SIGSEGV 定位):下载链路诊断日志——起播即留档,断流/崩溃后从 log 回溯。 */ meow_log(MEOW_LOG_INFO, "play: dl start id=%s lv=%s eff=%s size=%lld resume=%s url=%.56s", id, ncm_api_level_key(want), ncm_api_level_key(eff >= 0 ? eff : want), (long long)u.size, resuming ? "yes" : "no", u.url); /* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。 * size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */ size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20); ss->maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap; ncm_strlcpy(ss->url, u.url, sizeof ss->url); if (ncm_thread_create(&ss->dl_thr, dl_thread, ss) != 0) { ncm_cache_abort(ss->w); ss->w = NULL; stream_src_close(ss->src); ss->src = NULL; return NCM_ERR_IO; } ss->dl_thr_on = 1; /* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。 * still_intent 护栏覆盖本阻塞点:等待期间切歌/停止/同曲重播 → 保留 .part 静默放弃。 */ for (;;) { int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100); if (wr != 0) break; if (g_app_quit || !still_intent(idx, seq)) { stream_src_abort(ss->src); pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; ncm_cache_abort_part(ss->w); ss->w = NULL; /* 切歌/退出:保留残体供下次续传 */ stream_src_close(ss->src); ss->src = NULL; return DL_ABORTED; /* 调用方 still_intent 复核后静默返回 */ } } if (stream_src_aborted(ss->src)) { int frc = stream_src_failed(ss->src); pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; ncm_cache_abort_part(ss->w); ss->w = NULL; /* 中止:保留残体 */ stream_src_close(ss->src); ss->src = NULL; return frc ? frc : NCM_ERR_NET; } if (stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES) { int frc = stream_src_failed(ss->src); if (real_err) *real_err = frc; pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; /* 预缓冲前网络失败:删残体(不保留 .part),交调用方整首重试(含续传失败重下)。 * 已下过门槛的断流照常放行——读端播到截断点自然结束(F5)。 */ ncm_cache_abort(ss->w); ss->w = NULL; stream_src_close(ss->src); ss->src = NULL; return DL_RETRYABLE; } /* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */ *bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src); return NCM_OK; } /* 得到本地音频源(§21 边下边播)。命中缓存 → 静态 stream_src(final=size,read 不阻塞, * 顺带修 P1:不再 mmap 整文件);否则 URL 降级 → 起下载线程写 .tmp → 预缓冲 ≥128KB 即返回 * (下载在后台继续,解码经 stream_src read_cb 跟播)。 * 返回 NCM_OK 或负错误码;成功时 ss->src 就绪、ss->total 与 *bytes 回填(下载未完成时 * total=song_url 标称 size,0=未知)。s 为播放曲快照(run_song 物化,与窗口解耦 §17)。 * §20 权限/会员: * - fee 门禁(BIZ_NOTES §3 社区口径):fee∈{1,4}(VIP 才能听/付费专)且当前非会员 * → 直接 NCM_ERR_VIP,**不发 song_url 请求**(省一次请求,防风控);fee=0/8 放行 * (8=免费听低音质,播放允许)。本地缓存命中优先于门禁(缓存可播不拦)。 * - VIP 档上限:用户档超会员权限不硬拒,播放时先钳到会员允许的最高档再逐级降级 * (免去注定空 URL 的高档请求);实际生效档回填 g_play.eff_level。 */ static int song_local_file(const ncm_song_t *s, int idx, int seq, song_src_t *ss, char *path, size_t psz, int64_t *bytes) { memset(ss, 0, sizeof *ss); const char *id = s->id; char key[64]; audio_cache_key(key, sizeof key, id, g_level); /* 缓存命中 → 静态 stream_src(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */ int hit = 0; if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", path, psz) == NCM_OK || ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", path, psz) == NCM_OK) { hit = 1; } else if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) { /* §19 前存量:无档位后缀的 song_[.]mp3(含历史双点名 song_..mp3)。 * §NEW 任务5:命中即**迁移重命名**到当前档位键 song__.mp3(下方 hit==2 * 分支做 rename,不重下),之后不再回退旧键。 */ hit = 2; } if (hit) { struct stat st; if (stat(path, &st) != 0) return NCM_ERR_NOENT; if (hit == 2) { /* §NEW 任务5:存量旧键(无档位后缀 song_.mp3 / 历史双点 song_..mp3) * 迁移重命名到当前档位键 song__.(**不重下**,仅改名)。 * 目标已存在(该档已有正式条目)则删旧;迁移失败回退旧路径照常播。 */ char nk[64], np[512]; audio_cache_key(nk, sizeof nk, id, g_level); const char *dot = strrchr(path, '.'); const char *ext = dot ? dot + 1 : "mp3"; if (ncm_cache_path(&g_st.ctx.cache, nk, ext, np, sizeof np) == NCM_OK) { struct stat nst; if (stat(np, &nst) == 0) unlink(path); /* 目标已在(或并发建成):删旧双点/旧名 */ else rename(path, np); ncm_strlcpy(path, np, psz); /* 迁移后即用新路径(无论 rename 成败, * 覆盖到同一目标;失败则下次会再试) */ if (stat(np, &st) != 0) return NCM_ERR_NOENT; } } /* §NEW 任务2:静默降级时代产物修复(用户实锤:hires 档键下存着 mp3,播放命中旧档)。 * 用户档 > EXHIGH(无损/更高)且缓存实体是 .mp3 = flac 门禁前把 exhigh 的 mp3 写进了 * 无损域键(当年降级产物归用户档键)。迁移重命名到 exhigh 键 song__exhigh.mp3 * (选「极高」档时仍可用;目标已存在则删旧),然后本档视为未命中走下载—— * 311 的 flac 门禁会拿到真无损或正确降级。放在 hit==2 迁移块之后、静态流打开之前。 */ const char *mdot = strrchr(path, '.'); if (g_level > NCM_LEVEL_EXHIGH && mdot && ncm_streq(mdot, ".mp3")) { char kh[64], ph[512]; audio_cache_key(kh, sizeof kh, id, NCM_LEVEL_EXHIGH); if (ncm_cache_path(&g_st.ctx.cache, kh, "mp3", ph, sizeof ph) == NCM_OK) { struct stat hst; if (stat(ph, &hst) == 0) unlink(path); /* exhigh 目标已在:删旧(降级产物) */ else rename(path, ph); meow_log(MEOW_LOG_INFO, "play: stale-degraded id=%s lv=%s entity=mp3 -> exhigh key; redownload", id, ncm_api_level_key(g_level)); hit = 0; /* 本档不符 → 走下载(flac 门禁取真档) */ } } if (hit) { if (stream_src_open_static(&ss->src, path, (uint64_t)st.st_size) != 0) return NCM_ERR_IO; ss->total = (int64_t)st.st_size; *bytes = ss->total; /* 生效档推断(缓存键=用户档,降级产物归用户档键,无法精确还原): * mp3 实体 + 用户档 > 极高 → 必为降级产物,标 EXHIGH;其余按用户档显示。 * (flac 实体视为与用户档一致;hires→lossless 档内降级会高估显示,已知取舍。) * 注:mp3 + 用户档 > 极高 的组合已被上方迁移块拦截走下载,此处仅存防御。 */ const char *dot = strrchr(path, '.'); int eff = g_level; if (dot && ncm_streq(dot, ".mp3") && g_level > NCM_LEVEL_EXHIGH) eff = NCM_LEVEL_EXHIGH; pthread_mutex_lock(&g_play.mu); g_play.eff_level = eff; pthread_mutex_unlock(&g_play.mu); return NCM_OK; } } /* fee 门禁:VIP 才能听(1=VIP 歌曲 / 4=付费数字专辑)且非会员 → 不请求 url。 * (必须在缓存查找之后:已缓存的过期/下架曲本地可播,NCM_CACHE_DESIGN §2.4。) */ if ((s->fee == 1 || s->fee == 4) && account_vip() < NCM_VIP_CVIP) return NCM_ERR_VIP; /* 播放下载(阶段3 重试策略 §4.1,风控 §7:真实 NCM 请求节制):用户档先按会员权限 * 钳顶,再逐档降级(§20)。预缓冲前网络失败重试 STREAM_RETRY_MAX 次(退避 * STREAM_RETRY_BACKOFF_MS 防连环轰炸),重试每次重新 song_url(新签名 URL,eapi 便宜); * 硬错误/切歌/退出不重试。返回值:NCM_OK=src 就绪;DL_ABORTED=切歌/退出;其它负=错误。 */ int want = g_level; int vip_cap = account_level_cap(); if (want > vip_cap) want = vip_cap; int last_rc = NCM_ERR_NET; for (int attempt = 0; attempt <= STREAM_RETRY_MAX; attempt++) { if (attempt > 0) { meow_log(MEOW_LOG_WARN, "play: download retry %d/%d id=%s backoff=%dms", attempt, STREAM_RETRY_MAX, id, STREAM_RETRY_BACKOFF_MS); usleep(STREAM_RETRY_BACKOFF_MS * 1000); /* 退避:不轰炸 */ if (g_app_quit || !still_intent(idx, seq)) return NCM_ERR_IO; } int real_err = NCM_ERR_NET; int rc = song_download_attempt(s, idx, seq, key, want, ss, path, psz, bytes, &real_err); if (rc == NCM_OK) return NCM_OK; if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */ return rc; /* DL_ABORTED / 硬错误:不重试 */ } return last_rc; /* 重试耗尽:最后一次网络错误 */ } static void set_err(int err, const char *msg) { pthread_mutex_lock(&g_play.mu); g_play.err = err; if (msg) { ncm_strlcpy(g_play.errmsg, msg, sizeof g_play.errmsg); } g_play.state = NCM_PC_STOPPED; pthread_mutex_unlock(&g_play.mu); } /* 一首歌:源就绪(缓存/下载线程+预缓冲)→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */ static void run_song(int idx) { /* §17 滑窗兼容:物化目标曲(窗内命中 / 盘缓存 / song_detail 单曲)并整体快照进 * g_play.play_song——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */ /* §25:本条 run_song 服务的 play intent 用 seq 标识(play/next 递增)。切音质后重播 * 同一曲(同 idx 再 play)也会 seq++,故「我的 intent 是否仍有效」必须以 seq 判别 * (仅 cur/state 不够:同 idx 重播时 cur 不变、state 仍 BUFFER,详见 still_intent)。 */ pthread_mutex_lock(&g_play.mu); int my_seq = g_play.seq; int my_cur = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); if (my_cur != idx) return; /* 外层已同步过,防御 */ ncm_song_t s; if (appstate_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */ set_err(NCM_ERR_ARG, "load failed"); return; } if (!still_intent(idx, my_seq)) return; /* 物化期间已切歌/重播:不写过期快照 */ pthread_mutex_lock(&g_play.mu); g_play.play_song = s; g_play.play_song_valid = 1; pthread_mutex_unlock(&g_play.mu); mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */ song_src_t ss; int64_t file_bytes = 0; char path[512]; int rc = song_local_file(&s, idx, my_seq, &ss, path, sizeof path, &file_bytes); if (rc != NCM_OK) { if (!still_intent(idx, my_seq)) return; /* 期间已切歌/同曲重播:静默放弃(已落盘部分不亏) */ const char *m = (rc == NCM_ERR_VIP) ? "VIP only (fee gate)" : (rc == NCM_ERR_NOTFOUND) ? "no stream (VIP/copyright)" : (rc == NCM_ERR_AUTH) ? "session expired" : "load failed"; set_err(rc, m); return; } if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */ song_src_dispose(&ss); song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */ mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */ return; } ncm_strlcpy(g_path, path, sizeof g_path); g_file_bytes = file_bytes; /* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读 * 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part * (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */ ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path); if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path); else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path); meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0), gov_read_pos, ss.src); int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0; /* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb * SEEK_TO_BYTE|DO_NOT_SCAN);rate/ch/bits 按实际流。 */ ncm_decoder_t *dec = NULL; int rate = 0, ch = 0, bits = 0; int64_t total_frames = 0; if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) { song_src_dispose(&ss); song_gov_cleanup(&ss); mem_watch_song_end(); /* 释放钩子 */ if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */ set_err(NCM_ERR_IO, "decode open failed"); return; } if (dur <= 0 && total_frames > 0 && rate > 0) dur = total_frames * 1000 / rate; /* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */ if (audio_out_open(rate, ch, bits) != 0) { ncm_dec_close(dec); song_src_dispose(&ss); song_gov_cleanup(&ss); mem_watch_song_end(); /* 释放钩子 */ set_err(NCM_ERR_IO, "audio open failed"); return; } /* 状态→播放 */ pthread_mutex_lock(&g_play.mu); g_play.state = NCM_PC_PLAYING; g_play.pos_ms = 0; 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 fmt=%s %dHz/%dch/%dbit dur=%lldms level=%s eff=%s", idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur, ncm_api_level_key(g_level), ncm_api_level_key(g_play.eff_level >= 0 ? g_play.eff_level : g_level)); /* 单块缓冲:4096 帧 × ≤2ch × 4B(S32 上限)= 32KB,静态(播放线程单实例,省 32KB 栈)。 */ static int32_t pcm[PCM_CHUNK_FRAMES * 2]; int64_t frames_done = 0; /* 已输出帧(每通道) */ int need_seek_ms = -1; int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */ int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */ int underruns_seen = 0; int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */ for (;;) { if (g_app_quit) break; /* 取命令 */ pthread_mutex_lock(&g_play.mu); int cmd = g_play.cmd; g_play.cmd = 0; int64_t seek_ms = g_play.cmd_seek_ms; int cur = g_play.cur_song; int st = g_play.state; int seq = g_play.seq; pthread_mutex_unlock(&g_play.mu); if (cur != idx) { meow_log(MEOW_LOG_INFO, "play: switch idx=%d -> %d", idx, cur); break; } /* 切歌 → 回外层 */ if (st == NCM_PC_STOPPED) { meow_log(MEOW_LOG_INFO, "play: stopped idx=%d", idx); break; } if (seq != my_seq) { /* §25:同 idx 新播放意图(切音质后重播同一曲)——seq 变 * 即新 intent,必须打断当前曲回外层重跑(换档重播)。 */ meow_log(MEOW_LOG_INFO, "play: new intent idx=%d seq=%d->%d -> restart", idx, my_seq, seq); break; /* 当前曲(旧档)立即停,外层按新 cur/state 重跑 run_song */ } if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; } if (st == NCM_PC_PAUSED) { if (!paused_edge) { /* 进暂停:停 PCM 时钟但不丢流(audiod MSG_AUDIO_PAUSE) */ paused_edge = 1; meow_log(MEOW_LOG_INFO, "play: paused idx=%d pos=%lldms", idx, rate > 0 ? (long long)(frames_done * 1000 / rate) : 0); audio_out_pause(1); } usleep(40 * 1000); continue; /* 冻结 */ } if (paused_edge) { /* 出暂停:恢复 audiod 流(流死则自动重开) */ paused_edge = 0; meow_log(MEOW_LOG_INFO, "play: resumed idx=%d", idx); if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; } } if (need_seek_ms >= 0) { /* seek:flac 精确帧 / mp3 字节近似(MP3D_SEEK_TO_BYTE 直接 seek,无需关重开); * 目标超已下载区时 read_cb 自然阻塞等待(F4),输出重开 */ meow_log(MEOW_LOG_INFO, "play: seek idx=%d -> %dms", idx, need_seek_ms); audio_out_close(); if (ncm_dec_seek_ms(dec, need_seek_ms, g_file_bytes, dur) != 0) { set_err(NCM_ERR_IO, "seek failed"); break; } if (audio_out_open(rate, ch, bits) != 0) { set_err(NCM_ERR_IO, "audio reopen failed"); break; } frames_done = (int64_t)need_seek_ms * rate / 1000; need_seek_ms = -1; continue; } /* §21 停滞联动:read_cb 单次等待 >1s(audiod 流饥饿风险)→ 先对 audiod pause(1) * 保住流(暂停路径现状已有),数据恢复后 pause(0)(流死自动重开)。 */ if (stream_src_take_stalled(ss.src) && !stalled_edge) { stalled_edge = 1; meow_log(MEOW_LOG_INFO, "play: stalled idx=%d -> audio pause(1)", idx); audio_out_pause(1); pthread_mutex_lock(&g_play.mu); g_play.stalled = 1; pthread_mutex_unlock(&g_play.mu); } int64_t frames = ncm_dec_read(dec, pcm, PCM_CHUNK_FRAMES); if (frames == 0) { /* 播完(下载中断时=截断点,F5) */ /* §NEW 任务1(SIGSEGV 定位):播完时区分自然完成 vs 截断(下载未满/失败)。 * expected=标称 size;final=实际落盘;failed<0=中断。供真机崩溃回溯。 */ int64_t fin = (int64_t)stream_src_final(ss.src); int frc = stream_src_failed(ss.src); int64_t exp = ss.total > 0 ? ss.total : 0; meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld fin=%lld exp=%lld fail=%d underrun=%d", idx, (long long)frames_done, (long long)fin, (long long)exp, frc, (int)stream_src_written(ss.src)); break; } /* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」 * 自动顺延下一首,流死时连环跳歌并狂发 song_url 请求(§18 修)。 */ if (audio_out_write(pcm, (int)frames) != (int)frames) { if (!g_app_quit) set_err(NCM_ERR_IO, "audio write failed"); break; } frames_done += frames; if (stalled_edge) { /* 数据恢复:解除停滞 pause */ stalled_edge = 0; meow_log(MEOW_LOG_INFO, "play: stall recovered idx=%d -> audio pause(0)", idx); if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; } pthread_mutex_lock(&g_play.mu); g_play.stalled = 0; pthread_mutex_unlock(&g_play.mu); } /* underrun 统计落 g_play(§21,设置页/日志可见;变化才持锁写) */ { int u; uint64_t wm; stream_src_stats(ss.src, &u, &wm); if (u != underruns_seen) { underruns_seen = u; pthread_mutex_lock(&g_play.mu); g_play.underruns = u; g_play.underrun_ms = wm; pthread_mutex_unlock(&g_play.mu); meow_log(MEOW_LOG_INFO, "play: underrun idx=%d count=%d wait=%llums", idx, u, (unsigned long long)wm); } } /* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */ { int rf = stream_src_refaults(ss.src); if (rf != refaults_seen) { refaults_seen = rf; pthread_mutex_lock(&g_play.mu); g_play.refaults = rf; pthread_mutex_unlock(&g_play.mu); meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf); } meow_cachegov_tick(); } /* 更新进度(~200ms 节流)*/ int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0; pthread_mutex_lock(&g_play.mu); g_play.pos_ms = pos; if (dur > 0 && pos >= dur) g_play.pos_ms = dur; pthread_mutex_unlock(&g_play.mu); } audio_out_close(); ncm_dec_close(dec); /* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */ song_src_dispose(&ss); song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */ mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程 * join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */ /* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */ } /* 外层播放线程循环 */ static void *player_thread(void *arg) { (void)arg; while (g_want_run && !g_app_quit) { pthread_mutex_lock(&g_play.mu); int cur = g_play.cur_song; int st = g_play.state; pthread_mutex_unlock(&g_play.mu); if (cur < 0 || cur >= appstate_queue_count() || st == NCM_PC_STOPPED) { usleep(60 * 1000); continue; } /* 以本地快照播放(idx 被外层线程独占,无需再锁) */ run_song(cur); pthread_mutex_lock(&g_play.mu); int cur2 = g_play.cur_song; int st2 = g_play.state; pthread_mutex_unlock(&g_play.mu); /* 自然播完(decode 循环正常退出,state 仍 PLAYING)且无新命令 → 顺延下一首; * 若是因同 idx 重播(seq 变、state=BUFFER)返回,则循环顶会按当前 cur/state 重跑 * run_song(§25:切音质后重播同一曲换档生效),**不得**当播完顺延下一首。 */ int qc = appstate_queue_count(); if (cur2 >= 0 && cur2 < qc && st2 == NCM_PC_PLAYING && cur2 == cur) { int nx = cur + 1; if (nx < qc) { player_manager_next(1); } else { player_manager_stop(); } } else { usleep(50 * 1000); } } return NULL; } /* ---- 歌词后台加载(二期):UI 不阻塞 —— 持久 worker 监视当前播放歌曲,首播/切歌/自动顺延 * 时在独立线程拉取并解析,UI 经 lyric_state() 判「加载中 / 无词」。 ---- * §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[24]; /* 歌词归属歌曲 id */ static void *lyric_worker(void *arg) { (void)arg; char last_id[24] = ""; while (!g_app_quit) { usleep(300 * 1000); 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 >= appstate_queue_count() || !have) { lyric_clear(); pthread_mutex_lock(&g_lyrmu); g_lyr_song_id[0] = 0; g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu); last_id[0] = 0; continue; } if (!id[0]) continue; if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */ if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取 * (A 回退硬暂停 OR svc_perf C Cached 降载), * 不置 last_id → 降载恢复后下轮再拉。 */ ncm_strlcpy(last_id, id, sizeof last_id); lyric_clear(); pthread_mutex_lock(&g_lyrmu); g_lyr_state = 1; /* 加载中 */ pthread_mutex_unlock(&g_lyrmu); char *lrc = (char *)malloc(64 * 1024); char *tl = (char *)malloc(32 * 1024); if (!lrc || !tl) { free(lrc); free(tl); pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu); continue; } int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024); if (rc == NCM_OK) lyric_parse(lrc, tl); free(lrc); free(tl); pthread_mutex_lock(&g_lyrmu); ncm_strlcpy(g_lyr_song_id, id, sizeof g_lyr_song_id); g_lyr_state = 2; /* 拉完(可能 0 行=无词) */ pthread_mutex_unlock(&g_lyrmu); } return NULL; } static void lyric_ensure_worker(void) { pthread_mutex_lock(&g_lyrmu); if (!g_lyr_thr_on) { g_lyr_thr_on = 1; if (ncm_thread_create(&g_lyr_thr, lyric_worker, NULL) == 0) pthread_detach(g_lyr_thr); else g_lyr_thr_on = 0; } pthread_mutex_unlock(&g_lyrmu); } /* 为当前曲目「异步」拉歌词:确保后台 worker 在跑即返回(UI 不阻塞网络/解析)。 * 实际拉取+解析在 worker 线程(约 0.3s 内开始),结束置 lyric_state()=2。 */ int player_manager_load_lyric(void) { lyric_ensure_worker(); return NCM_OK; } /* 歌词加载状态(UI 判「loading / no lyric」)。 */ int lyric_state(void) { int s; pthread_mutex_lock(&g_lyrmu); s = g_lyr_state; pthread_mutex_unlock(&g_lyrmu); return s; } int player_manager_start(void) { if (g_thr_on) return 0; g_want_run = 1; g_level = g_st.level; g_cache_gb = g_st.cache_limit_gb; pthread_mutex_init(&g_play.mu, NULL); meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */ mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */ if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1; g_thr_on = 1; return 0; } void player_manager_stop_thread(void) { g_want_run = 0; g_app_quit = 1; audio_out_close(); /* 打断阻塞写 */ mem_watch_stop(); /* 停监控线程 */ if (g_thr_on) { pthread_join(g_thr, NULL); g_thr_on = 0; } }