- 解码分发 decoder.c:magic(fLaC→dr_flac / 其它→minimp3);flac ≤16bit→S16、 >16bit→S32 满幅送 audiod(AUDIO_FMT_S32),rate/ch/bits 按实际流,零 SRC 零裁剪 - song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变;flac 下载限幅 1GB - 缓存键 song_<id>_<level>.<ext>:同歌同档命中不重下,换档新条目旧档留 LRU; ext 前导点正名(修双点名),lookup 回退历史双点名+旧无档键温和兼容 - audio_out S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原格式 - 验证(0 真实请求):44.1k/16 与 96k/24 自制样本宿主+qemu 帧数/时长精确、 逐样本零误差;mp3 回归逐帧一致;player_flow_sim P1-P6(新 P6 档位键/flac ext)、 cache_evict/cover/pagination/window/audiod_chain sim 全 PASS;mipsel 0 warning - REPORT §19 / VENDOR_NOTES dr_flac 条目;未部署真机(留主会话)
521 lines
21 KiB
C
521 lines
21 KiB
C
/* play/player_manager.c — 播放线程:URL 获取(Hi-res 逐档降级)→缓存(命中直接播/否则流式落盘)→
|
||
* minimp3 解码→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),旧无档位键温和兼容。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
#include "decoder.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; }
|
||
|
||
/* 本曲下载进度(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++;
|
||
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++;
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||
static int download_cb(void *ctx, const unsigned char *data, size_t len)
|
||
{
|
||
/* 下载进度反馈(UI「缓冲中… N%」,§18):回调在播放线程,锁内仅累加。 */
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.dl_done += (int64_t)len;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
return ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
|
||
}
|
||
|
||
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
|
||
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。 */
|
||
static void audio_cache_key(char *out, size_t sz, const char *id, int level)
|
||
{
|
||
snprintf(out, sz, "%s_%s", id, ncm_api_level_str(level));
|
||
}
|
||
|
||
/* 得到本地音频路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
|
||
* 返回 NCM_OK 或负错误码。id 由调用方(run_song)从播放曲快照取,与窗口解耦(§17)。 */
|
||
static int song_local_file(const char *id, char *path, size_t psz, int64_t *bytes)
|
||
{
|
||
char key[64];
|
||
audio_cache_key(key, sizeof key, id, g_level);
|
||
|
||
/* 缓存命中 → 离线直接播(新键两扩展名都试;档位=用户设定档,降级产物也归该档键) */
|
||
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) {
|
||
struct stat st;
|
||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||
}
|
||
/* 旧键温和兼容(§19 前存量:无档位后缀的 song_<id>[.]mp3,含历史双点名):命中即播,
|
||
* 不主动改名/删除,由 LRU 自然淘汰。 */
|
||
if (ncm_cache_lookup(&g_st.ctx.cache, id, "mp3", path, psz) == NCM_OK) {
|
||
struct stat st;
|
||
if (stat(path, &st) == 0) { *bytes = (int64_t)st.st_size; return NCM_OK; }
|
||
}
|
||
|
||
/* 播放地址:从 g_level 降级,接受 mp3 与 flac(§19:dr_flac 落地后无损/Hi-Res 真实生效) */
|
||
ncm_play_url_t u;
|
||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, g_level, &u);
|
||
if (rc != NCM_OK) return rc;
|
||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||
/* 落盘扩展名: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";
|
||
pthread_mutex_lock(&g_play.mu);
|
||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
|
||
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
|
||
ncm_cache_evict(&g_st.ctx.cache);
|
||
|
||
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
|
||
ncm_cache_writer_t *w = NULL;
|
||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &w);
|
||
if (rc != NCM_OK) return rc;
|
||
|
||
ncm_http_req_t req;
|
||
memset(&req, 0, sizeof req);
|
||
req.url = u.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 = 30000;
|
||
|
||
ncm_http_resp_t resp;
|
||
/* 上限:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB),flac 上限 1GB;mp3 维持 64MB。
|
||
* size 可信时按 size 限幅(防超额),未知按格式默认上限。 */
|
||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||
size_t maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap;
|
||
rc = ncm_http_download(&req, download_cb, w, maxb, &resp);
|
||
if (rc != NCM_OK || resp.status != 200) {
|
||
ncm_cache_abort(w);
|
||
return (rc != NCM_OK) ? rc : NCM_ERR_HTTP;
|
||
}
|
||
rc = ncm_cache_commit(w);
|
||
if (rc != 0) return rc;
|
||
/* 写后检查:新落盘可能把池推过上限 → 再按 LRU 淘汰最旧(刚写的 mtime 最新,存活) */
|
||
ncm_cache_evict(&g_st.ctx.cache);
|
||
if (ncm_cache_lookup(&g_st.ctx.cache, key, ext, path, psz) != NCM_OK)
|
||
return NCM_ERR_NOENT;
|
||
struct stat st;
|
||
if (stat(path, &st) == 0) *bytes = (int64_t)st.st_size;
|
||
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);
|
||
}
|
||
|
||
/* 切歌竞态护栏(§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;
|
||
}
|
||
|
||
/* 一首歌:下载→解码→输出,直到结束或被打断。返回后由外层处理下一首。 */
|
||
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);
|
||
|
||
int64_t file_bytes = 0;
|
||
char path[512];
|
||
int rc = song_local_file(s.id, path, sizeof path, &file_bytes);
|
||
if (rc != NCM_OK) {
|
||
if (!still_current(idx)) return; /* 下载期间已切歌:静默放弃(缓存已落盘不亏) */
|
||
const char *m = (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)) 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):magic 探测 fLaC→dr_flac / 其它→minimp3;rate/ch/bits 按实际流。 */
|
||
ncm_decoder_t *dec = NULL;
|
||
int rate = 0, ch = 0, bits = 0;
|
||
int64_t total_frames = 0;
|
||
if (ncm_dec_open(&dec, path, &rate, &ch, &bits, &total_frames) != 0) {
|
||
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); 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",
|
||
idx, s.id, ncm_dec_format(dec), rate, ch, bits, (long long)dur);
|
||
|
||
/* 单块缓冲: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 */
|
||
|
||
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 关+重开按字节近似(decoder 内处理);输出重开 */
|
||
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;
|
||
}
|
||
|
||
int64_t frames = ncm_dec_read(dec, pcm, PCM_CHUNK_FRAMES);
|
||
if (frames == 0) { /* 播完 */
|
||
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;
|
||
/* 更新进度(~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);
|
||
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 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;
|
||
}
|
||
}
|