From 30ffe469c8be6845b5490f8520456392c8a3d2ed Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 4 Nov 2026 20:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(ncm):=20=E7=BC=93=E5=AD=98=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E6=A1=A3=E5=88=A4=E5=AE=9A/=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=E8=A7=A3=E6=9E=90=20+=20=E6=9F=A5=E6=89=BE=E6=A1=A3?= =?UTF-8?q?=E4=BD=8D=E4=B8=8B=E6=8E=A2=EF=BC=88M6=20core=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ncm_cache_actual_level:请求档 + 容器/码率/位深 + 响应生效档 → 实际档(只降不升), 作为写盘名/播放显示/已缓存列表的唯一事实源 - ncm_cache_level_from_path:文件名档位段解析(song__<档位>,含 sky_c51) - ncm_cache_lookup:请求档精确 miss 后按实际档逐档下探(hires 命中已存 exhigh/lossless), 只降不升,低档请求不命中高档实体 - ncm_cache_scan:actual_level 改用统一判定;同曲同实际档的旧名+新名去重展示 --- patch_v2/ncm_app/core/ncm_cache.c | 173 ++++++++++++++++++++++++------ patch_v2/ncm_app/core/ncm_cache.h | 33 +++++- 2 files changed, 169 insertions(+), 37 deletions(-) diff --git a/patch_v2/ncm_app/core/ncm_cache.c b/patch_v2/ncm_app/core/ncm_cache.c index 806fdb9..19d18ea 100644 --- a/patch_v2/ncm_app/core/ncm_cache.c +++ b/patch_v2/ncm_app/core/ncm_cache.c @@ -148,10 +148,12 @@ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL; } -int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, - char *out, size_t out_sz) { +/* 单键精确查找(含 §19 历史双点名 `song_..` → 单点名的迁移重命名)。 + * 命中:out=正式单点名路径并刷新 mtime(LRU),返回 NCM_OK;未命中 NCM_ERR_NOENT。 */ +static int cache_lookup_exact(const ncm_cache_t *c, const char *key, const char *ext, + char *out, size_t out_sz) { char path[NCM_CACHE_DIR_MAX + 32]; - if (ncm_cache_path(c, id, ext, path, sizeof path) != 0) return NCM_ERR_FULL; + if (ncm_cache_path(c, key, ext, path, sizeof path) != 0) return NCM_ERR_FULL; struct stat st; if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { /* §19 温和兼容:历史双点名 `song_..`(旧版 ext 带前导点所致)。 @@ -160,27 +162,23 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, const char *e = ext; if (e && e[0] == '.') e++; const char *ee = e ? e : "bin"; - const char *base = cache_base_dir(c, id); - int n = snprintf(path, sizeof path, "%s/song_%s..%s", base, id, ee); + const char *base = cache_base_dir(c, key); + int n = snprintf(path, sizeof path, "%s/song_%s..%s", base, key, ee); if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL; if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { /* §NEW 分目录兜底:迁移前旧布局双点名可能仍躺在缓存根(init 迁移后应已在子目录; * 这里双点兼容:子目录先查、根目录再查,保证任何存量双点都被迁移修正)。 */ - n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, ee); + n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, key, ee); if (n < 0 || (size_t)n >= sizeof path) return NCM_ERR_FULL; if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT; } char fixed[NCM_CACHE_DIR_MAX + 32]; - if (ncm_cache_path(c, id, ee, fixed, sizeof fixed) == NCM_OK) { + if (ncm_cache_path(c, key, ee, fixed, sizeof fixed) == NCM_OK) { struct stat fst; if (stat(fixed, &fst) == 0) unlink(path); /* 目标已(在/被并发建):删旧双点 */ else rename(path, fixed); - if (stat(fixed, &st) == 0 && S_ISREG(st.st_mode)) { - ncm_strlcpy(path, fixed, sizeof path); - } else { - ncm_strlcpy(path, fixed, sizeof path); /* 迁移失败(权限等):回到固定路径 */ - return NCM_ERR_NOENT; - } + ncm_strlcpy(path, fixed, sizeof path); /* 迁移失败(权限等):回到固定路径 */ + if (stat(fixed, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT; } } utime(path, NULL); /* 刷新最后访问(按 mtime LRU) */ @@ -188,6 +186,51 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, return NCM_OK; } +/* 档位下探表(高→低;DOLBY 与降级链/UI 同口径不在表内)。缓存文件按**实际档**命名(M6), + * 故高档请求可下探命中已存的低档实体:hires 请求命中 song__exhigh.mp3(降级产物不重下)。 */ +static const int k_level_ladder[] = { + NCM_LEVEL_SURROUND, NCM_LEVEL_SKY, NCM_LEVEL_JYMASTER, NCM_LEVEL_JYEFFECT, + NCM_LEVEL_HIRES, NCM_LEVEL_LOSSLESS, NCM_LEVEL_EXHIGH, NCM_LEVEL_HIGHER, + NCM_LEVEL_STANDARD, +}; +#define LEVEL_LADDER_N ((int)(sizeof k_level_ladder / sizeof k_level_ladder[0])) + +/* 解析歌曲缓存键 `<数字id>_<档位键>` → 档位枚举(id 回填)。非歌曲键/无档位 → -1。 */ +static int split_level_key(const char *key, char *id, size_t id_sz) { + const char *p = key; + size_t n = 0; + if (!key || !key[0]) return -1; + while (p[n] >= '0' && p[n] <= '9' && n + 1 < id_sz) { id[n] = p[n]; n++; } + if (n == 0 || p[n] != '_') return -1; + id[n] = 0; + return ncm_api_level_from_key(p + n + 1); +} + +int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, + char *out, size_t out_sz) { + int rc; + if (!c || !id || !out) return NCM_ERR_ARG; + rc = cache_lookup_exact(c, id, ext, out, out_sz); + if (rc == NCM_OK || rc == NCM_ERR_FULL) return rc; + if (id_is_cover(id)) return NCM_ERR_NOENT; /* 封面键无档位语义 */ + char sid[24]; + int lv = split_level_key(id, sid, sizeof sid); + if (lv < 0 || lv > NCM_LEVEL_SURROUND) return NCM_ERR_NOENT; + int idx = -1; + for (int i = 0; i < LEVEL_LADDER_N; i++) + if (k_level_ladder[i] == lv) { idx = i; break; } + if (idx < 0) return NCM_ERR_NOENT; + for (int i = idx + 1; i < LEVEL_LADDER_N; i++) { /* 只降不升 */ + char k2[48]; + int n = snprintf(k2, sizeof k2, "%s_%s", sid, ncm_api_level_key(k_level_ladder[i])); + if (n < 0 || (size_t)n >= sizeof k2) continue; + rc = cache_lookup_exact(c, k2, ext, out, out_sz); + if (rc == NCM_OK) return NCM_OK; + if (rc == NCM_ERR_FULL) return rc; + } + return NCM_ERR_NOENT; +} + struct ncm_cache_writer { char final_path[NCM_CACHE_DIR_MAX + 64]; char tmp_path[NCM_CACHE_DIR_MAX + 80]; @@ -465,10 +508,11 @@ int ncm_cache_clear(const ncm_cache_t *c) { } /* ---- 「已缓存列表」:扫描/删除(键=歌曲ID+档位,NCM_CACHE_DESIGN §2.5)---- - * 文件名 song__<请求档位>.<实际格式>;**档位段只是请求档**,实际档由文件头探测得出 - * (真机 song__hires.mp3 = 无损请求被降级链落到 exhigh mp3)。 */ + * 文件名 song__<档位>.<实际格式>:M6 起**档位段=实际档**(响应档),如无损请求被 + * 降级链落到 exhigh mp3 → song__exhigh.mp3。旧版缓存名的档位段仍是请求档 + * (真机 song__hires.mp3),读取/删除/LRU 一律按文件名原样兼容,不强制迁移。 */ -/* 解析条目名 → id/请求档位键/实际扩展名。song_ 后必须是数字 id(排除 song_cov* 封面)。 +/* 解析条目名 → id/文件名档位键/实际扩展名。song_ 后必须是数字 id(排除 song_cov* 封面)。 * 返回 0=是歌曲条目。 */ static int parse_item_name(const char *nm, ncm_cache_item_t *it) { @@ -493,23 +537,70 @@ static int parse_item_name(const char *nm, ncm_cache_item_t *it) return 0; } -/* 请求档 + 实际容器参数 → 实际档(只降不升:显示「已降级至 X」语义)。 */ -static int actual_level_of(int req, const ncm_audio_info_t *ai) +/* 容器探测结果 → 该容器可承载的档位上界(-1=无法判定,由响应档/请求档决定)。 */ +static int container_level_cap(const ncm_audio_info_t *ai) { - int cap; - if (!ai || !ai->container[0]) return req; /* 探测失败:保留请求档 */ + if (!ai || !ai->container[0]) return -1; if (strcmp(ai->container, "flac") == 0) { - if (ai->bits_per_sample <= 16) cap = NCM_LEVEL_LOSSLESS; - else if (ai->sample_rate > 96000) cap = NCM_LEVEL_JYMASTER; /* 192k/24 */ - else if (ai->sample_rate > 48000) cap = NCM_LEVEL_JYEFFECT; /* 96k/24 */ - else cap = NCM_LEVEL_HIRES; /* 48k/24 */ - } else if (strcmp(ai->container, "mp3") == 0) { - cap = (ai->bitrate_kbps > 128) ? NCM_LEVEL_EXHIGH : NCM_LEVEL_STANDARD; - } else { - return req; + if (ai->bits_per_sample <= 0 || ai->sample_rate <= 0) return -1; /* 仅知容器 */ + if (ai->bits_per_sample <= 16) return NCM_LEVEL_LOSSLESS; + if (ai->sample_rate > 96000) return NCM_LEVEL_JYMASTER; /* 192k/24 */ + if (ai->sample_rate > 48000) return NCM_LEVEL_JYEFFECT; /* 96k/24 */ + return NCM_LEVEL_HIRES; /* 48k/24 */ } - if (req < 0) return cap; - return (req < cap) ? req : cap; + if (strcmp(ai->container, "mp3") == 0) { + if (ai->bitrate_kbps > 128) return NCM_LEVEL_EXHIGH; + if (ai->bitrate_kbps > 0) return NCM_LEVEL_STANDARD; + return -1; /* 码率未知:交 resp_level 钳顶 */ + } + return -1; +} + +int ncm_cache_actual_level(int req, const ncm_audio_info_t *ai, int resp_level) +{ + int cap = container_level_cap(ai); + int v; + if (resp_level < 0) resp_level = req; + if (ai && strcmp(ai->container, "mp3") == 0) { + if (cap < 0) cap = NCM_LEVEL_EXHIGH; /* mp3 永不高于极高 */ + if (resp_level >= 0 && resp_level < cap) cap = resp_level; + } else if (cap < 0) { + cap = resp_level; /* 头参数未知:信响应/降级链生效档 */ + } else if (resp_level >= 0 && resp_level < cap) { + cap = resp_level; + } + if (req < 0) v = (cap < 0) ? -1 : cap; + else if (cap < 0) v = req; + else v = (req < cap) ? req : cap; /* 只降不升 */ + return v; +} + +int ncm_cache_level_from_path(const char *path) +{ + const char *base, *dot, *us = NULL, *us2 = NULL; + char key[20]; + size_t n; + int lv; + if (!path || !path[0]) return -1; + base = strrchr(path, '/'); + base = base ? base + 1 : path; + dot = strrchr(base, '.'); + if (!dot) dot = base + strlen(base); + for (const char *p = base; p < dot; p++) + if (*p == '_') { us2 = us; us = p; } + if (!us || us + 1 >= dot) return -1; + n = (size_t)(dot - us - 1); + if (n >= sizeof key) return -1; + memcpy(key, us + 1, n); + key[n] = 0; + lv = ncm_api_level_from_key(key); + if (lv >= 0 || !us2 || us2 + 1 >= us - 1) return lv; + /* 档位键本身带下划线(sky_c51):再带上前一段试一次 */ + n = (size_t)(dot - us2 - 1); + if (n >= sizeof key) return -1; + memcpy(key, us2 + 1, n); + key[n] = 0; + return ncm_api_level_from_key(key); } static int cmp_item_mtime_desc(const void *a, const void *b) @@ -559,8 +650,11 @@ int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max) slot = oldest; } memset(&ai, 0, sizeof ai); - it.actual_level = actual_level_of(ncm_api_level_from_key(it.level), - (ncm_audio_probe_file(p, &ai) == NCM_OK) ? &ai : NULL); + /* 文件名档位 + 文件头实测 → 实际档(新命名两者一致;旧请求档命名由探测纠正, + * 如 song__hires.mp3 实体是 320k mp3 → actual=exhigh)。 */ + it.actual_level = ncm_cache_actual_level(ncm_api_level_from_key(it.level), + (ncm_audio_probe_file(p, &ai) == NCM_OK) ? &ai : NULL, + -1); if (ncm_tag_read_file(p, &tag) == NCM_OK) { /* 文件内 tag 优先(外部播放器同源) */ ncm_strlcpy(it.name, tag.title, sizeof it.name); ncm_strlcpy(it.artist, tag.artist, sizeof it.artist); @@ -570,6 +664,21 @@ int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max) } closedir(d); qsort(out, (size_t)n, sizeof *out, cmp_item_mtime_desc); + /* 同曲同实际档去重展示:旧命名(请求档)与新命名(实际档)可能同时存在 + * (如 song__hires.mp3 与 song__exhigh.mp3 都是 320k mp3),只留最近访问的一条。 */ + { + int w = 0; + for (int i = 0; i < n; i++) { + int dup = 0; + for (int j = 0; j < w; j++) { + if (strcmp(out[j].id, out[i].id) == 0 && + out[j].actual_level == out[i].actual_level && + strcmp(out[j].ext, out[i].ext) == 0) { dup = 1; break; } + } + if (!dup) { if (w != i) out[w] = out[i]; w++; } + } + n = w; + } return n; } diff --git a/patch_v2/ncm_app/core/ncm_cache.h b/patch_v2/ncm_app/core/ncm_cache.h index c485edf..0bf1ef3 100644 --- a/patch_v2/ncm_app/core/ncm_cache.h +++ b/patch_v2/ncm_app/core/ncm_cache.h @@ -11,6 +11,9 @@ #include #include +#include "ncm_api.h" /* NCM_LEVEL_*(实际档位判定/文件名档位解析) */ +#include "ncm_tag.h" /* ncm_audio_info_t(容器/位深/码率探测结果) */ + #ifdef __cplusplus extern "C" { #endif @@ -47,10 +50,28 @@ int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz); /* 查找已缓存文件。命中:写 out 路径并刷新 mtime,返回 NCM_OK。未命中:NCM_ERR_NOENT。 - * §19:ext 前导点被忽略(新旧调用方同路径);单点 miss 时回退历史双点名 `song_..`。 */ + * §19:ext 前导点被忽略(新旧调用方同路径);单点 miss 时回退历史双点名 `song_..`。 + * + * 档位下探(M6):键形如 `<歌曲id>_<档位键>` 时,请求档精确 miss 后**只降不升**逐档下探 + * (hires → lossless → exhigh → higher → standard)。缓存文件已按**实际档**命名,故 + * hires 请求能命中已存的 song__exhigh.mp3(降级产物不重下);低档请求不会命中高档实体。 */ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz); +/* 实际档位判定(**单一事实源**:写缓存名 / 已缓存列表显示 / 命中档位推断共用)。 + * req 请求档 NCM_LEVEL_*(<0=未知) + * ai 容器探测结果(可为 NULL;flac 需 bits/sample_rate 才按头判档) + * resp_level 响应/降级链生效档(<0=未知;flac 响应头参数未知时以它为准) + * 返回 min(req, 容器可承载档, resp_level)——只降不升: + * mp3 码率 ≤128k → standard,>128k → exhigh(mp3 永不高于 exhigh); + * flac 16bit/44.1k → lossless,24bit/48k → hires,96k → jyeffect,192k → jymaster。 */ +int ncm_cache_actual_level(int req, const ncm_audio_info_t *ai, int resp_level); + +/* 从缓存文件路径解析**文件名档位段**(song__<档位>. 的档位;取扩展名前最后一个 + * '_' 之后的部分)。新命名=实际档,旧命名=请求档(可能存在降级,调用方需再按容器钳顶)。 + * 返回 NCM_LEVEL_*;无档位段/未知档位 → -1。 */ +int ncm_cache_level_from_path(const char *path); + /* 流式写入句柄:open 建 temp;write 累积(write(2) 直写无 stdio 缓冲,§21 边下边播 * 读端即时可见);commit 落盘+刷新LRU+触发淘汰;abort 丢弃。 */ typedef struct ncm_cache_writer ncm_cache_writer_t; @@ -88,12 +109,13 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c); int ncm_cache_evict(const ncm_cache_t *c); /* 缓存条目(ncm_cache_scan 输出,供「已缓存列表」展示/点播/删除)。 - * 命名约定 song__<请求档位>.<实际格式>;**档位段是请求档,不是实际档**——真机存在 - * song_171650_hires.mp3(无损请求被降级链落到 exhigh mp3)。故 scan 用 ncm_audio_probe_file - * 读实际容器参数,actual_level 给**实际档**(-1=无法判定);播放/删除仍用 id+level+ext 定位。 */ + * 命名约定 song__<档位>.<实际格式>(M6 起档位段=**实际档**;旧版缓存名的档位段仍是请求档, + * 如真机 song_171650_hires.mp3 = 无损请求被降级链落到 exhigh mp3,读可用、不强制迁移)。 + * 故 level 只作「文件名档位键」用于定位删除/点播;actual_level 由 ncm_audio_probe_file 读实际 + * 容器参数后按 ncm_cache_actual_level 判定(新命名下与文件名一致;-1=无法判定)。 */ typedef struct ncm_cache_item { char id[24]; /* 歌曲 id(数字串) */ - char level[20]; /* 文件名里的请求档位键(""=旧无档位键条目) */ + char level[20]; /* 文件名档位键(新命名=实际档;旧命名=请求档;""=旧无档位键条目) */ char ext[8]; /* 实际容器扩展名(mp3/flac,去点) */ int actual_level; /* 实际档 NCM_LEVEL_*(按容器/位深/码率推断;-1=未知) */ uint64_t size; /* 文件字节数(含嵌入 tag) */ @@ -104,6 +126,7 @@ typedef struct ncm_cache_item { /* 扫描歌曲缓存池(songs/,跳过 .tmp/.part 半成品与非 song_<数字 id> 条目),按 mtime 降序 * (最近播放/写入在前)写入 out(最多 max 条;池内更多时保留最近的 max 条)。返回条目数或负错误码。 + * 同一 (id, actual_level, ext) 只保留最近访问的一条(旧请求档名 + 新实际档名并存时去重展示)。 * 每条会读文件头(tag + 音频参数探测),属磁盘 IO——**必须在 worker 线程调用**。 */ int ncm_cache_scan(const ncm_cache_t *c, ncm_cache_item_t *out, int max);