/* 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。 */ #include "../ncm_app_internal.h" #include #include #include #include #include #include "decoder.h" #include "stream_src.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; /* 被抢占回调(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); pthread_mutex_unlock(&g_play.mu); } void player_manager_play(int song_idx) { pthread_mutex_lock(&g_play.mu); 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; 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; 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; 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; } pthread_mutex_unlock(&g_play.mu); } void player_manager_stop(void) { pthread_mutex_lock(&g_play.mu); 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):读端 stream_src + 下载路径写端/线程。 * run_song 栈上分配,下载线程 join 后才返回,生命周期安全。 */ #define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */ 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 分母) */ } song_src_t; /* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 + * 锁内推进 written + broadcast 唤醒读端 + 进度反馈。 */ 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 */ 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"; req.extra_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.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; /* 绝对段(连接/握手/头)上限;body 空闲超时 15s(P2) */ ncm_http_resp_t resp; int rc = ncm_http_download(&req, stream_dl_cb, ss, ss->maxb, &resp); if (rc == NCM_OK && resp.status != 200) 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(unlink .tmp)。阶段 2 语义:中断即丢弃;.part 续传/LRU 防淘汰留阶段 3。 */ 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,此处已近曲终) */ if (ncm_cache_commit(ss->w) == 0) ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */ } else { ncm_cache_abort(ss->w); } ss->w = NULL; } if (ss->src) { stream_src_close(ss->src); ss->src = NULL; } } /* 缓存键(用户定案 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)); } /* 切歌竞态护栏(§18):物化/下载/预缓冲期间用户可能已切歌——只有仍是当前目标且处于 * BUFFER(本次播放意图未过期)才继续,否则直接回外层(外层会以新 cur 重跑 run_song)。 */ static int still_current(int idx) { pthread_mutex_lock(&g_play.mu); int ok = (g_play.cur_song == idx) && (g_play.state == NCM_PC_BUFFER); pthread_mutex_unlock(&g_play.mu); return 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, 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,含历史双点名): * 命中即播,不主动改名/删除,由 LRU 自然淘汰。 */ hit = 2; } if (hit) { struct stat st; if (stat(path, &st) != 0) return NCM_ERR_NOENT; 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 档内降级会高估显示,已知取舍。) */ 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; /* 播放地址:用户档先按会员权限钳顶,再逐档降级(§20 全档表),接受 mp3 与 flac */ int want = g_level; int vip_cap = account_level_cap(); if (want > vip_cap) want = vip_cap; ncm_play_url_t u; int eff = -1; int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, 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 >= 0) ? eff : want; 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 淘汰(此线程尚未开 temp,无在途文件被误删风险) */ ncm_cache_evict(&g_st.ctx.cache); /* 边下边播(§21):下载线程写 .tmp(write(2) 直写),解码线程 pread 同一文件 */ rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w); if (rc != NCM_OK) return rc; if (stream_src_open_stream(&ss->src, ncm_cache_writer_tmp_path(ss->w)) != 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; /* 上限:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB),flac 上限 1GB;mp3 维持 64MB。 * size 可信时按 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 (pthread_create(&ss->dl_thr, NULL, 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_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 中止下载静默放弃。 */ for (;;) { int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100); if (wr != 0) break; if (g_app_quit || !still_current(idx)) { stream_src_abort(ss->src); pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; ncm_cache_abort(ss->w); ss->w = NULL; stream_src_close(ss->src); ss->src = NULL; return NCM_ERR_IO; /* 调用方 still_current 复核后静默返回 */ } } if (stream_src_aborted(ss->src) || (stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES)) { /* 中止,或首开声前失败(written < 预缓冲门槛):abort .tmp *(阶段 2 不保留不重试;重试策略留阶段 3)。已下过门槛的断流照常放行—— * 读端播到截断点自然结束(F5)。 */ int frc = stream_src_failed(ss->src); pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0; ncm_cache_abort(ss->w); ss->w = NULL; stream_src_close(ss->src); ss->src = NULL; return frc ? frc : NCM_ERR_NET; } /* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */ *bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src); return NCM_OK; } 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——之后本曲所有字段读快照,窗口滑出当前播放曲不影响播放。 */ 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; } } if (!still_current(idx)) return; /* 物化期间已切歌/停止:不写过期快照 */ pthread_mutex_lock(&g_play.mu); g_play.play_song = s; g_play.play_song_valid = 1; pthread_mutex_unlock(&g_play.mu); song_src_t ss; int64_t file_bytes = 0; char path[512]; int rc = song_local_file(&s, idx, &ss, path, sizeof path, &file_bytes); if (rc != NCM_OK) { if (!still_current(idx)) 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_current(idx)) { /* 预缓冲完成但已切歌:勿开解码/勿动状态 */ song_src_dispose(&ss); return; } ncm_strlcpy(g_path, path, sizeof g_path); g_file_bytes = file_bytes; 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); if (!still_current(idx)) 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); 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; 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; 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 (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) */ meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done); 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); } } /* 更新进度(~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); /* 若应继续播下一首且非因命令打断,由外层处理(外层见 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 >= g_st.track_ids_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); /* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */ if (cur2 >= 0 && cur2 < g_st.track_ids_count && st2 != NCM_PC_STOPPED && cur2 == cur) { int nx = cur + 1; if (nx < g_st.track_ids_count) { 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 >= g_st.track_ids_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; /* 同一曲已拉/在拉 */ 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 (pthread_create(&g_lyr_thr, NULL, 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 读线程分发) */ if (pthread_create(&g_thr, NULL, 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(); /* 打断阻塞写 */ if (g_thr_on) { pthread_join(g_thr, NULL); g_thr_on = 0; } }