Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/play/player_manager.c
T
MeowOS 1e97dbf54a ncm search: 历史+单曲/歌单分类+分页+误触修复+不覆写歌单上下文 (§23)
- 搜索历史:进页自动弹键盘;meow_config 持久化 ≤12 去重最新在前;空态=历史列表(点搜/左滑删/清空)
- 结果分类单曲/歌单页签:独立结果集+独立滚动;歌单行进详情(playlist_open_pl);单曲行播放
- 触底分页:offset 累加 limit=30/页 cloudsearch 同参数;每类各记 offset
- 误触抑制:g_press_row + g_press_delta>10(对齐 home/playlist §13.3)
- 结果改独立数组(不写 g_st.songs[]);播放搜索单曲建搜索队列(queue_src=SEARCH),不弄丢歌单窗口
- core cloudsearch 扩展 type/offset/limit/total + 歌单解析
- bench/search_sim.c 离线回归(历史/独立集/翻页 offset/队列上下文);host searchtap 误触自检

验证:mipsel -Wall -Wextra 0 warning;core host/mipsel PASS;search_sim PASS;
宿主离屏 search(单曲)/searchpl(歌单)/searchhist(空态历史)/searchtap(误触) 均正常;0 真实 NCM 请求。
2026-09-01 19:21:16 +08:00

710 lines
32 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#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;
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
if (c >= cnt) c = cnt - 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_<id>_<level>.<ext>"。
* 同歌同档命中不重复下载;换档=新条目,旧档留给 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_<id>[.]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_queue_song(idx, &s) != 0) { /* §23:按 queue_src 取(窗口 或 搜索队列) */
set_err(NCM_ERR_ARG, "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 >= 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);
/* 自然播完且无新命令 → 顺延下一首;否则回到循环等待 */
int qc = appstate_queue_count();
if (cur2 >= 0 && cur2 < qc && st2 != NCM_PC_STOPPED && 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; /* 同一曲已拉/在拉 */
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;
}
}