ncm: FLAC 解码落地(dr_flac 0.13.4 单头入库) + 缓存键=歌曲ID+音质档位

- 解码分发 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 条目;未部署真机(留主会话)
This commit is contained in:
MeowOS committed 2026-09-01 14:14:38 +08:00
1 parent 80bc191593
commit 70aec677ce
23 files changed
+13377 -110

No files matched your search

+3 -1
View File
@@ -75,7 +75,9 @@ font/src/**/*.ttc
patch_v2/sdk/test_p1p2p3_repro
# ncm_app vendor 源码与构建产物(体积大;版本/获取方式见 VENDOR_NOTES.md,构建脚本可复现)
patch_v2/ncm_app/vendor/
# 例外:dr_flac 单头文件入库(§19 FLAC 解码依赖,532KB 单文件可入库)
patch_v2/ncm_app/vendor/*
!patch_v2/ncm_app/vendor/dr_flac/
patch_v2/ncm_app/out/
patch_v2/sdk/test_sdk_fixes
+155
View File
@@ -1586,3 +1586,158 @@ fee4204e945866f5a07ca7d191fa83e3 bench/player_flow_sim.c
挂机时封面可能缺席但不影响播放。
- 播放页 BUFFER 态进度条仍画 0:00 标尺(缓冲文案在歌词区),未做进度条一体化——观感项。
- meow_audiod_host 工作区有他人改动(其他 agent 在改 audiod),本批未 add/未提交该文件。
---
## 19. FLAC 解码落地(dr_flac)+ 缓存键 = 歌曲ID+音质档位(2026-09-01)
> 范围:仅 `patch_v2/ncm_app/` + 根 `.gitignore`(vendor/dr_flac 例外条目)。未动 `sdk/`、
> `meow_audiod/`、`meow_launcherd/`。风控纪律:本批真实 NCM 请求 **0 次**(全部离线 stub/sim/
> 自制测试样本;song_url 降级链只改接受条件,未新增请求路径)。
> **本批未部署真机**(部署留主会话统一做)。
### 19.1 动机
播放链此前只有 minimp3(mp3-only):`song_url_auto` 只收 mp3 URL,无损 SQ/Hi-Res 档位
拿到 flac 也只能继续降级 → 无损/Hi-Res 空转(NCM_CACHE_DESIGN §2.5「同日发现」)。
硬件侧 CS43198 支持到 768kHz/S24,audiod 已支持 S16/S24/S32 输入 + DAC 原生率跟随
(AUDIOD_DESIGN §4),缺的只是解码器。本批引入 dr_flac 并打通全链。
### 19.2 dr_flac 引入(vendor/dr_flac/dr_flac.h,入库)
- 来源:https://github.com/mackron/dr_libs(master 分支 `dr_flac.h`,2026-09-01 取得,
532768 B,版本宏 0.13.4);许可证 **public domain / MIT-0 任选**(文件头/尾双许可声明)。
- 单头文件直接入库:`.gitignore` 由 `patch_v2/ncm_app/vendor/` 改为 `vendor/*` +
`!vendor/dr_flac/` 例外(其余 vendor 大树维持不入库,VENDOR_NOTES.md 记版本/获取方式)。
- 集成:`app/play/dr_flac_impl.c` 唯一实现 TU(`DR_FLAC_IMPLEMENTATION` +
`DR_FLAC_NO_OGG`——song_url 下发的 flac 恒原生封装,裁 ogg 容器省 text),
编译豁免告警(与 minimp3 同口径)。
### 19.3 解码分发(app/play/decoder.{h,c},新)
- `ncm_dec_open` 按 **magic** 分发:首 4 字节 `fLaC` → dr_flac;其余(ID3 / 0xFF 帧同步/
未知)→ minimp3(未知格式由 minimp3 open 报错兜底,不加第三分支)。
- 统一接口:`open(回填 rate/ch/bits/total_frames)` / `read(帧)` / `seek_ms` / `close` /
`format`。位宽映射:flac ≤16bit → S16;>16bit → **S32 左对齐满幅**(drflac s32 输出),
送 audiod `AUDIO_FMT_S32`——格式归一/SRC-free 原生率跟随全在 audiod 侧(AUDIOD §4),
本层零裁剪零 SRC。mp3 恒 S16。
- mp3 细节:minimp3_ex 的 `dec.info` 首读后才有效 → open 时预读一块进 stash(4096 帧),
read 先吐 stash;seek 保持旧语义(关+重开按字节近似)。flac seek = 精确 PCM 帧。
- `player_manager.c` run_song 改走 decoder:`audio_out_open(rate, ch, bits)` 按实际流
(不再硬编码 44100/2/16);进度 pos/暂停日志/seek 帧数全部用实际 rate;
song 元数据 duration 缺失时用 `total_frames/rate` 推准确时长(flac 精确)。
单块 PCM 缓冲改静态 32KB(4096 帧 ×2ch ×4B S32 上限;播放线程单实例,省栈)。
### 19.4 audio_out 位宽参数化(S32 通路)
- `audio_out_write` 签名 `const short *` → `const void *`(字节数按 open 的 bits×ch 算,
FILE/ALSA 两后端本就如此)。
- AUDIOD 后端:bits 16→`AUDIO_FMT_S16`、32→`AUDIO_FMT_S32`(其余拒绝);流死重开
(pause resume 失败路径)按本次 bits 还原格式(旧版恒 S16,flac 曲重开会错格式——随本批修)。
- ALSA 直开保底:支持 S32_LE(16/32);WAV sink 头 bits 字段如实写(16/32 均合法 PCM WAV)。
### 19.5 song_url 降级链接收 flac(core/ncm_api.c)
- `ncm_api_song_url_auto` 接受条件:`type ∈ {mp3, flac}`;`type` 缺失(旧端点兼容)时看
url 扩展名(`.flac` → 回填 type=flac),默认当 mp3 收。降级语义不变(hires→lossless→
exhigh→higher→standard 逐档,url null/未知格式继续降)。
- 下载限幅:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB)→ flac 上限 1GB、
mp3 维持 64MB(`u.size` 可信时按 size 限幅)。
### 19.6 缓存键 = 歌曲ID + 音质档位(用户定案 NCM_CACHE_DESIGN §2.5)
- 新键:`song_<id>_<level>.<ext>`(level = `ncm_api_level_str`:standard/higher/exhigh/
lossless/hires)。**同歌同档命中不重复下载;换档=新条目;旧档留给 LRU 自然淘汰**(不主动删)。
键取**用户设定档**(非实际降级产物档)——保证「同设定永不重下」,降级产物归设定档键。
- 查找顺序:新键 flac → 新键 mp3 → 旧无档位键 `song_<id>.mp3`(存量温和兼容,命中即播)。
- **命名正名**:`ncm_cache_path` 剥离 ext 至多一个前导点——旧版调用方传 ".mp3" 生成
双点名 `song_<id>..mp3`(§256 修的既有约定),新文件一律单点;`ncm_cache_lookup`
单点 miss 时回退历史双点名(命中照常刷 LRU,不改名不删除)。封面池(song_cov*.jpg)
同享正名+双点回退。
- autoplay hook(`/tmp/ncm_autoplay`)的「已缓存」判定同步三键顺序。
### 19.7 验证记录(0 真实 NCM 请求)
测试样本:libsndfile(venv soundfile)自制 `out/test-vectors/sweep_44k1_16.flac`
(44.1kHz/16bit/2ch/3.000s=132300 帧)与 `sweep_96k_24.flac`(96kHz/24bit/2ch/2.000s=
192000 帧)(正弦扫频;out/ 不入库,脚本见 §19.9 可复现)。
| 项 | 方式 | 结果 |
|---|---|---|
| flac 44.1k/16 宿主 | `test_decode`(新:走 decoder 分发→audio_out FILE WAV),expect 132300/44100/16 | **PASS**:帧数/率/位宽精确;WAV 数据与 libsndfile 参考**逐样本零误差** |
| flac 96k/24 宿主 | 同上,expect 192000/96000/32 | **PASS**:S32 满幅输出,换算 float 与参考 max err = 0.0;173× RT |
| mp3 回归 宿主 | MIPSTest.mp3 expect 393984/48000/16 | **PASS**:与 §16 记录逐帧一致(393984 帧/8.208s),WAV 字节数精确 |
| flac+mp3 qemu-mipsel | `test_decode_mipsel`(静态交叉)同三样本 | **全 PASS**:44.1k 50.9× RT / 96k24 29.0× RT / mp3 4.8× RT(qemu 模拟保守值,真机更快);MIPS32 LE 端序/32位 long 无问题 |
| 播放状态机 | `bench/player_flow_sim.c`(stub 换 ncm_dec_*;**新增 P6**:档位键+flac ext) | **P1-P6 全 PASS**:P6 实证 cache key=`1000_lossless`、type=flac → `.flac` 落盘;P1-P5 旧用例零回归 |
| 缓存键/命名 | `test_ncm_core`(新增:ext 带点正名单点路径 + 历史双点名 lookup 命中)+ `cache_evict_sim` | 宿主 + qemu-mipsel **全 PASS**;evict 6 组 PASS(`song_*` 前缀扫描不受键变化影响) |
| 封面/滑窗回归 | cover_chain_sim / pagination_sim / window_sim(重编新 ncm_cache.c/appstate.c) | **全 PASS**(封面池同享正名+双点回退;autoplay 三键判定 gc 段不影响 sim) |
| audiod 链回归 | audiod_chain_sim(真实 audiod_host 假 socket,重编新 audio_out.c) | **14/14 PASS**(OPEN/WRITE/PAUSE/RESUME/抢占/流死重开/CLOSE) |
| mipsel 构建 | `build_ncm.sh`(自有代码 -Wall -Wextra;dr_flac_impl vendor 豁免) | **0 warning**;`text 954304 / data 1576 / bss 1113668`(text +85.5KB = dr_flac+decoder) |
| 宿主离屏渲染 | build_ncm_host.sh + 9 页 MEOW_UI_DUMP | 全 rc=0;自有代码 0 warning(SDK meow_ui_vol.c 既有告警除外) |
### 19.8 md5(本批产物 + 改动文件)
```
52d20f5325ba2bb646355dcc6db54ad3 ncm # mipsel 静态 App(新;未部署,留主会话)
ff7acea1b1769fb2e67a5177dfc37d8f ncm_host # 宿主离屏调试二进制
90c7901bff0ff6e2e579ef068ac48161 vendor/dr_flac/dr_flac.h
86e99aaad27bfc828a59694022e4a8aa app/play/decoder.c
b02e8e5f7c7194a6bf492171c137233f app/play/decoder.h
2c6d5b93cecec980a67b737329847905 app/play/dr_flac_impl.c
6b8b0d13933a547c3349e423a9516eb3 app/play/player_manager.c
6f5ea39ab6b7ef54c87c48916c8705eb app/play/audio_out.c
2ad8b12f47014d034df75f864cb2a9b9 app/ncm_app_internal.h
2ca5b164877617063a7a44964acdf3d8 core/ncm_api.c
de21f105514235594f90427beec0b10f core/ncm_api.h
b870b25c9bd9692c912170ddce2b04de core/ncm_cache.c
0cb40a5af7f1c85af79f4f4b44a9cc4a core/ncm_cache.h
811c730b5fc52256f84b8917b7657649 app/logic/appstate.c
fba1d01fe6f3cfc9bcf334481e8edb09 app/test/test_decode.c
0ed4ce768d82a9512c5d32bca1271366 test/test_ncm_core.c
c6846cb4c31161c42d81560ade628771 bench/player_flow_sim.c
791a9ddbde363348e3ef373931647aeb build_ncm.sh
f4fb97416868b9a61b6b54f8958bc6f9 build_ncm_host.sh
65e3da445996e62079bfa38a9c9699b7 VENDOR_NOTES.md
```
### 19.9 出入清单(只改 `patch_v2/ncm_app/` + 根 .gitignore)
```
新增:
vendor/dr_flac/dr_flac.h # dr_flac 0.13.4 单头(public domain/MIT-0;入库例外)
app/play/decoder.h/.c # 格式分发解码(magic:fLaC→dr_flac / 其它→minimp3;S16/S32 位宽映射)
app/play/dr_flac_impl.c # dr_flac 唯一实现 TU(DR_FLAC_NO_OGG;vendor 豁免告警)
修改:
app/play/player_manager.c # run_song 走 decoder(实际 rate/ch/bits/时长)+ 缓存键 id+档位 +
# flac 落盘(ext 按 type/url)+ flac 下载限幅 1GB + 旧无档键兼容
app/play/audio_out.c # write 改 const void* + S32 通路(AUDIOD/ALSA/WAV)+ 流死重开按 bits 还原
app/ncm_app_internal.h # audio_out_write 签名/注释
core/ncm_api.c/.h # song_url_auto 接受 flac(type 权威/url 扩展名兜底),降级语义不变
core/ncm_cache.c/.h # ext 前导点正名(单点)+ lookup 历史双点名回退
app/logic/appstate.c # autoplay 已缓存判定三键顺序(新档键 flac/mp3 → 旧无档键)
app/test/test_decode.c # 改走 decoder 分发 + expect_frames/rate/bits 校验 + meow_ui_time_ms 桩
test/test_ncm_core.c # 缓存命名正名 + 双点旧名兼容用例
bench/player_flow_sim.c # stub 换 ncm_dec_* + ncm_api_level_str/ncm_streq 桩 + P6(档位键/flac ext)
build_ncm.sh / build_ncm_host.sh # +decoder.c +dr_flac_impl.c + -Ivendor/dr_flac
VENDOR_NOTES.md # dr_flac 条目(来源/版本/许可/集成方式/实测)
.gitignore(仓库根) # vendor/ → vendor/* + !vendor/dr_flac/(单头入库)
ncm / ncm_host # 重编产物
(未动 sdk/、meow_audiod/、meow_launcherd/、其余 vendor/;未部署真机;未动设备系统/恢复区)
```
### 19.10 诚实出入 / 遗留
- **真机未部署未验**(留主会话统一)。真机验收点:①无损/Hi-Res 档位真实出 flac 流并出声
(48k/16 与 96k/192k/24 各试一首,日志 `play: start ... fmt=flac <rate>Hz/2ch/16|32bit` +
audiod grant 跟随原生率)②换音质档后同歌重下新条目、旧档仍在(LRU 未触发时)③旧缓存
(无档键/双点名)直接可播不重下 ④flac seek 精确帧。
- **无损档位要会员**:song_url 的 lossless/hires 档需 CVIP/SVIP——**用户账号能否真拿到
flac URL 待真机按账号实测**(非会员会逐级降到 exhigh mp3,行为同旧版,不算回归)。
- 八档音质体系(NCM_CACHE_DESIGN §3 的 jyeffect/sky/jymaster 等 SVIP 档)未进 App
(设置仍 5 档,枚举只到 NCM_LEVEL_HIRES)——缓存键已按档字符串泛化,后续加档零改动。
- dr_flac 只接原生 flac 封装(NO_OGG);song_url 若下发 ogg-flac 会被 sniff 判给 minimp3
报错「decode open failed」——目前未见该形态(type=flac 恒原生),真机若见再加 ogg。
- 96k/24 → S32 满幅在 qemu 下 29× RT,真机 MIPS 预估实时余量充足;若实测吃紧可开
`DR_FLAC_NO_CRC`(省 CRC 校验,~5-10% CPU)。
- 测试样本(out/test-vectors/)与宿主/ffmpeg 工具链(out/tools/)在 out/ 下不入库;
样本可用 python `soundfile` 一键重造(sweep 参数见 §19.7)。
+11
View File
@@ -15,6 +15,7 @@
| cJSON | 1.7.18 | MIT | 20.0 KB | 参考对比(未选,太大) |
| qrcodegen | nayuki@993ccbb | MIT | 10.8 KB | ✅ 选(二维码) |
| minimp3 | master@ea99364 | CC0 | 37.4 KB | ✅ 选(MP3 v1 解码) |
| dr_flac | 0.13.4 | public domain / MIT-0 | 见 REPORT §19(§19 补选,FLAC 解码,单头入库) | ✅ 选(无损/Hi-Res) |
| dr_mp3 | 0.7.4 | MIT/public-domain | 40.7 KB | 参考对比(未选,质量更好但更大更吃内存) |
## 各库明细
@@ -63,6 +64,16 @@
- 上游:https://github.com/mackron/dr_libs(dr_mp3.h 单文件)。
- text=41679(≈40.7 KB);兼容性/音质更好,但更大、解码中间态内存更高。minimp3 更契合 14MB 限制。
### dr_flac 0.13.4 — public domain / MIT-0(§19 选,FLAC 解码)
- 上游:https://github.com/mackron/dr_libs(`dr_flac.h` 单头文件,532768 B,
2026-09-01 取自 master;版本宏 DRFLAC_VERSION=0.13.4)。**本文件入库**
(`.gitignore` 对 `vendor/` 开了 `!vendor/dr_flac/` 例外;单头文件可入库,网络不可达时可复现)。
- 集成:`app/play/dr_flac_impl.c` 唯一实现 TU(`DR_FLAC_IMPLEMENTATION` + `DR_FLAC_NO_OGG`
裁 ogg 容器——网易云 song_url 的 flac 恒原生封装),vendor 豁免告警(同 minimp3 口径)。
- 位宽:`drflac_read_pcm_frames_s16`(≤16bit)/ `s32`(>16bit,左对齐满幅 → audiod
AUDIO_FMT_S32,AUDIOD_DESIGN.md §4 归一在 SDK/audiod 侧)。
- 实测(REPORT §19):44.1k/16 与 96k/24 样本逐样本零误差;qemu-mipsel 96k/24 解码 29× RT。
## 备注(工程)
- mbedtls 产物默认写在 `vendor/mbedtls/library/`(3.6 的 Makefile 不理会 BUILD 变量),脚本再拷贝到 `out/`。
- `MBEDTLS_SSL_IN_CONTENT_LEN/OUT_CONTENT_LEN` 默认 16KB;对 14MB 也不大,可再按需调小(见 REPORT 内存估)。
+6 -2
View File
@@ -435,8 +435,12 @@ static int autoplay_try_playlist(int pl)
for (int i = 0; i < g_st.track_ids_count; i++) { /* 滑窗模型:锁内拷贝窗内有效槽 */
ncm_song_t s;
if (appstate_song_copy(i, &s) != 0) break; /* 出窗即停(首页之后未物化) */
if (ncm_cache_lookup(&g_st.ctx.cache, s.id, ".mp3",
cpath, sizeof cpath) == NCM_OK) {
/* §19 缓存键 = id+档位:先试当前档位键(flac/mp3),再回退旧无档位键(存量 mp3) */
char key[64];
snprintf(key, sizeof key, "%s_%s", s.id, ncm_api_level_str(g_st.level));
if (ncm_cache_lookup(&g_st.ctx.cache, key, "flac", cpath, sizeof cpath) == NCM_OK ||
ncm_cache_lookup(&g_st.ctx.cache, key, "mp3", cpath, sizeof cpath) == NCM_OK ||
ncm_cache_lookup(&g_st.ctx.cache, s.id, "mp3", cpath, sizeof cpath) == NCM_OK) {
g_st.active_playlist = pl;
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play",
pl, i, s.id);
+2 -2
View File
@@ -315,8 +315,8 @@ void cover_cache_reset(void);
/* ---- play(app/play/)---- */
/* audio_out(后端:默认 SINK_AUDIOD=meow_audiod 独占服务;env NCM_AUDIO_SINK=file/null/alsa
* 或 MEOW_AUDIO_DIRECT=1 选调试后端,见 audio_out.c 文件头) */
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功 */
int audio_out_write(const short *samples, int frames); /* <0=错误/结束 */
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功;bits∈{16,32}(§19 flac>16bit→32) */
int audio_out_write(const void *samples, int frames); /* <0=错误/结束;样本位宽=open 的 bits */
int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */
void audio_out_close(void);
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
+17 -11
View File
@@ -62,7 +62,7 @@ static void pivset(struct snd_interval *iv, unsigned val)
}
static int alsa_open(int rate, int ch, int bits)
{
if (bits != 16) return -1;
if (bits != 16 && bits != 32) return -1; /* §19:S16 + S32(flac >16bit 满幅) */
int fd = open("/dev/snd/pcmC0D0p", O_RDWR);
if (fd < 0) {
meow_log(MEOW_LOG_ERROR, "audio: alsa open /dev/snd/pcmC0D0p failed errno=%d", errno);
@@ -84,7 +84,8 @@ static int alsa_open(int rate, int ch, int bits)
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(am, 0, sizeof *am); pmask(am, SNDRV_PCM_ACCESS_RW_INTERLEAVED);
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(fm, 0, sizeof *fm); pmask(fm, SNDRV_PCM_FORMAT_S16_LE);
memset(fm, 0, sizeof *fm);
pmask(fm, bits == 32 ? SNDRV_PCM_FORMAT_S32_LE : SNDRV_PCM_FORMAT_S16_LE);
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL],
@@ -99,13 +100,14 @@ static int alsa_open(int rate, int ch, int bits)
return -1;
}
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0);
meow_log(MEOW_LOG_INFO, "audio: alsa open ok rate=%d ch=%d (direct debug)", rate, ch);
meow_log(MEOW_LOG_INFO, "audio: alsa open ok rate=%d ch=%d bits=%d (direct debug)", rate, ch, bits);
return fd;
}
#endif
/* 写 WAV 头(44 字节标准 PCM 布局:RIFF/WAVE/fmt(16)/data)。
* fmt(16) = format(2) ch(2) sample_rate(4) byte_rate(4) block_align(2) bits(2)。 */
* fmt(16) = format(2) ch(2) sample_rate(4) byte_rate(4) block_align(2) bits(2)。
* §19:bits 16/32 均合法(32=flac >16bit 的 S32 满幅样本)。 */
static void file_write_header(int fd, int rate, int ch, int bits, uint32_t data_len)
{
unsigned char h[44];
@@ -174,12 +176,16 @@ int audio_out_open(int sample_rate, int channels, int bits)
#endif
}
/* 默认:SINK_AUDIOD(meow_audiod 独占服务)。v1 SDK 声源格式 S16(minimp3 输出即 S16_LE)。 */
if (bits != 16) {
meow_log(MEOW_LOG_ERROR, "audio: audiod open: bits=%d unsupported (v1 S16 only)", bits);
/* 默认:SINK_AUDIOD(meow_audiod 独占服务)。§19:bits 16→S16 / 32→S32
* (flac >16bit 由 dr_flac 出 S32 满幅;格式归一/DAC 原生率跟随在 audiod 侧)。 */
int fmt;
if (bits == 16) fmt = AUDIO_FMT_S16;
else if (bits == 32) fmt = AUDIO_FMT_S32;
else {
meow_log(MEOW_LOG_ERROR, "audio: audiod open: bits=%d unsupported (16/32)", bits);
return -1;
}
int sid = meow_audio_open(sample_rate, channels, AUDIO_FMT_S16);
int sid = meow_audio_open(sample_rate, channels, fmt);
if (sid < 0) {
meow_log(MEOW_LOG_ERROR, "audio: audiod open failed rc=%d rate=%d ch=%d (audiod 不在?)",
sid, sample_rate, channels);
@@ -199,7 +205,7 @@ int audio_out_open(int sample_rate, int channels, int bits)
return 0;
}
int audio_out_write(const short *samples, int frames)
int audio_out_write(const void *samples, int frames)
{
if (g_app_quit) return -1;
if (g_sink == SINK_NULL || g_sink < 0) return frames;
@@ -252,11 +258,11 @@ int audio_out_pause(int paused)
meow_log(MEOW_LOG_WARN, "audio: audiod pause sid=%d rc=%d (stream dead?)", g_sid, rc);
return 0;
}
/* 恢复失败 = 流已死 → 重开 */
/* 恢复失败 = 流已死 → 重开(格式按本次 open 的 bits 还原:16→S16 / 32→S32) */
meow_log(MEOW_LOG_WARN, "audio: audiod resume sid=%d rc=%d -> reopen", g_sid, rc);
meow_audio_close(g_sid);
g_sid = 0;
int sid = meow_audio_open(g_rate, g_ch, AUDIO_FMT_S16);
int sid = meow_audio_open(g_rate, g_ch, g_bits == 32 ? AUDIO_FMT_S32 : AUDIO_FMT_S16);
if (sid < 0) {
meow_log(MEOW_LOG_ERROR, "audio: audiod reopen failed rc=%d rate=%d ch=%d", sid, g_rate, g_ch);
return -1;
+141
View File
@@ -0,0 +1,141 @@
/* play/decoder.c — 格式分发解码实现(接口见 decoder.h,REPORT §19)。
*
* mp3 细节:minimp3_ex 的 dec.info(hz/channels)在首次 mp3dec_ex_read 后才保证有效,
* 而调用方开 audio_out 前就要 rate/ch —— open 时先预读一批进 stash,read 先吐 stash。
* seek(mp3):关+重开(MP3D_SEEK_TO_BYTE 索引在 open 时重建)再按字节近似 seek,同旧语义。
*/
#include "decoder.h"
#include "minimp3_ex.h"
#include "dr_flac.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEC_STASH_FRAMES 4096 /* open 预读上限(帧),与播放循环单块读同量级 */
enum { DEC_MP3 = 0, DEC_FLAC = 1 };
struct ncm_decoder {
int kind;
char path[512];
int rate, ch, bits;
int64_t total_frames; /* 0=未知 */
/* mp3 */
mp3dec_ex_t mp3;
int16_t *stash; /* 预读样本(交织 s16),stash_n 个样本 */
size_t stash_n, stash_off;
/* flac */
drflac *flac;
};
/* magic 探测:首 4 字节 "fLaC" → flac;其余(ID3 / 0xFF 帧同步 / 未知)→ mp3
*(未知格式留给 minimp3 在 open 时报错,不在本层加第三种分支)。 */
static int sniff_kind(const char *path)
{
unsigned char m[4] = {0};
FILE *f = fopen(path, "rb");
if (f) { size_t n = fread(m, 1, 4, f); fclose(f); if (n < 4) return DEC_MP3; }
if (m[0] == 'f' && m[1] == 'L' && m[2] == 'a' && m[3] == 'C') return DEC_FLAC;
return DEC_MP3;
}
int ncm_dec_open(ncm_decoder_t **out, const char *path,
int *rate, int *ch, int *bits, int64_t *total_frames)
{
if (!out || !path) return -1;
ncm_decoder_t *d = (ncm_decoder_t *)calloc(1, sizeof *d);
if (!d) return -1;
d->kind = sniff_kind(path);
snprintf(d->path, sizeof d->path, "%s", path);
if (d->kind == DEC_FLAC) {
d->flac = drflac_open_file(path, NULL);
if (!d->flac) { free(d); return -1; }
d->rate = (int)d->flac->sampleRate;
d->ch = (int)d->flac->channels;
/* 位宽映射(AUDIOD §4 归一交给 SDK/audiod):≤16 → S16;>16 → S32 满幅 */
d->bits = (d->flac->bitsPerSample <= 16) ? 16 : 32;
d->total_frames = (int64_t)d->flac->totalPCMFrameCount;
} else {
if (mp3dec_ex_open(&d->mp3, path, MP3D_SEEK_TO_BYTE) != 0) { free(d); return -1; }
d->bits = 16;
d->stash = (int16_t *)malloc(DEC_STASH_FRAMES * 2 * sizeof(int16_t));
if (!d->stash) { mp3dec_ex_close(&d->mp3); free(d); return -1; }
/* 预读首块:填 dec.info(hz/channels)+ 预热 stash */
size_t got = mp3dec_ex_read(&d->mp3, d->stash, DEC_STASH_FRAMES * 2);
d->stash_n = got;
d->rate = d->mp3.info.hz;
d->ch = d->mp3.info.channels;
if (d->rate <= 0 || d->ch <= 0) { /* 首块即失败/空流 */
mp3dec_ex_close(&d->mp3); free(d->stash); free(d); return -1;
}
d->total_frames = (d->ch > 0) ? (int64_t)(d->mp3.samples / (uint64_t)d->ch) : 0;
}
if (d->ch < 1 || d->ch > 2) { ncm_dec_close(d); return -1; } /* v1 只接 单/双声道 */
*out = d;
if (rate) *rate = d->rate;
if (ch) *ch = d->ch;
if (bits) *bits = d->bits;
if (total_frames) *total_frames = d->total_frames;
return 0;
}
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames)
{
if (!d || !buf || cap_frames <= 0) return 0;
if (d->kind == DEC_FLAC) {
if (d->bits == 16)
return (int64_t)drflac_read_pcm_frames_s16(d->flac, (drflac_uint64)cap_frames,
(drflac_int16 *)buf);
return (int64_t)drflac_read_pcm_frames_s32(d->flac, (drflac_uint64)cap_frames,
(drflac_int32 *)buf);
}
/* mp3:先吐 stash,再直读 */
int16_t *out = (int16_t *)buf;
int64_t cap_samples = cap_frames * d->ch;
int64_t done = 0;
if (d->stash_off < d->stash_n) {
size_t left = d->stash_n - d->stash_off;
size_t take = left < (size_t)cap_samples ? left : (size_t)cap_samples;
memcpy(out, d->stash + d->stash_off, take * sizeof(int16_t));
d->stash_off += take;
done += (int64_t)take;
}
if (done < cap_samples) {
size_t got = mp3dec_ex_read(&d->mp3, out + done, (size_t)(cap_samples - done));
done += (int64_t)got;
}
return done / d->ch;
}
int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t file_bytes, int64_t dur_ms)
{
if (!d || ms < 0) return -1;
if (d->kind == DEC_FLAC) {
drflac_uint64 frame = (drflac_uint64)(ms * d->rate / 1000);
return drflac_seek_to_pcm_frame(d->flac, frame) ? 0 : -1;
}
/* mp3:关+重开(重建字节索引)再按字节近似 */
mp3dec_ex_close(&d->mp3);
memset(&d->mp3, 0, sizeof d->mp3);
if (mp3dec_ex_open(&d->mp3, d->path, MP3D_SEEK_TO_BYTE) != 0) return -1;
d->stash_n = d->stash_off = 0;
int64_t target = (file_bytes > 0 && dur_ms > 0) ? file_bytes * ms / dur_ms : 0;
return mp3dec_ex_seek(&d->mp3, (uint64_t)(target > 0 ? target : 0));
}
void ncm_dec_close(ncm_decoder_t *d)
{
if (!d) return;
if (d->kind == DEC_FLAC) { if (d->flac) drflac_close(d->flac); }
else { mp3dec_ex_close(&d->mp3); free(d->stash); }
free(d);
}
const char *ncm_dec_format(const ncm_decoder_t *d)
{
if (!d) return "?";
return d->kind == DEC_FLAC ? "flac" : "mp3";
}
+34
View File
@@ -0,0 +1,34 @@
/* play/decoder.h — 格式分发解码(REPORT §19):按文件 magic 选后端。
* "fLaC" → dr_flac(无损 SQ / Hi-Res,vendor/dr_flac.h)
* 其它(ID3 / 0xFF 帧同步 / 未知)→ minimp3(既有 mp3 路径,未知格式由 minimp3 兜底报错)
* 输出约定:bits<=16 → 交织 S16;bits>16 → 交织 S32(dr_flac s32 左对齐满幅,送 audiod
* AUDIO_FMT_S32,AUDIOD_DESIGN.md §4 格式归一由 SDK/audiod 完成,本层不做 SRC/裁剪)。
*/
#ifndef NCM_DECODER_H
#define NCM_DECODER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct ncm_decoder ncm_decoder_t;
/* 打开并探测流参数:rate/ch/bits/total_frames 经指针回填实际值(mp3 恒 16bit;
* total_frames 0=未知)。返回 0=成功。 */
int ncm_dec_open(ncm_decoder_t **out, const char *path,
int *rate, int *ch, int *bits, int64_t *total_frames);
/* 读最多 cap_frames 帧到 buf(交织,样本位宽=open 返回的 bits;cap 以帧计,
* buf 容量须 ≥ cap_frames*ch*(bits/8))。返回实际帧数,0=EOF。 */
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames);
/* seek 到 ms。flac=精确 PCM 帧;mp3=按 file_bytes*ms/dur_ms 字节近似(同旧语义)。 */
int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t file_bytes, int64_t dur_ms);
void ncm_dec_close(ncm_decoder_t *d);
/* "mp3" / "flac"(日志/UI 用)。 */
const char *ncm_dec_format(const ncm_decoder_t *d);
#ifdef __cplusplus
}
#endif
#endif /* NCM_DECODER_H */
+7
View File
@@ -0,0 +1,7 @@
/* play/dr_flac_impl.c — dr_flac 唯一实现 TU(header-only:define 实现一次,与
* minimp3_impl.c 同款)。编译时 -I<vendor>/dr_flac;vendor 豁免告警(同 minimp3 口径)。
* DR_FLAC_NO_OGG:ogg 封装的 flac 网易云不会下发(song_url type=flac 恒原生 flac),
* 裁掉 ogg 容器代码省 text。 */
#define DR_FLAC_NO_OGG
#define DR_FLAC_IMPLEMENTATION
#include "dr_flac.h"
+66 -34
View File
@@ -3,9 +3,12 @@
*
* 模型:一个持久的播放线程。外层循环取「目标曲目」,run_song() 负责一首歌的
* 「下载(缓冲) → 解码 → 输出 → 结束/被指令打断」。暂停=冻结(停读停写停推 pos,状态置 PAUSED);
* seek=重开解码器+输出并 seek 到目标 ms(字节 offset 近似);下一/上一首=打断当前 run_song 返回外层。
* 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>
@@ -13,15 +16,15 @@
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "minimp3_ex.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 8192 /* 每块交织样本数(双声道 → 4096 帧) */
#define PCM_CHUNK_FRAMES 4096 /* 每块帧数(≈旧 8192 样本/双声道) */
/* ---- 线程内部一次性歌的状态 ---- */
static char g_path[512]; /* 本地 mp3 路径(缓存) */
static char g_path[512]; /* 本地音频路径(缓存,mp3/flac) */
static int64_t g_file_bytes;
static int g_level; /* 音质档(来自设置) */
static int g_cache_gb;
@@ -142,21 +145,42 @@ static int download_cb(void *ctx, const unsigned char *data, size_t len)
return ncm_cache_write((ncm_cache_writer_t *)ctx, data, len);
}
/* 得到本地 mp3 路径 + 文件大小。命中缓存直接返回;否则 URL 降级 + 流式落缓存。
/* 缓存键(用户定案 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)
{
/* 缓存命中 → 离线直接播 */
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) == NCM_OK) {
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 */
/* 播放地址:从 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);
@@ -166,7 +190,7 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
/* 流式下载到缓存 temp → commit(稳妥者:先落盘再播) */
ncm_cache_writer_t *w = NULL;
rc = ncm_cache_open(&g_st.ctx.cache, id, ".mp3", &w);
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &w);
if (rc != NCM_OK) return rc;
ncm_http_req_t req;
@@ -179,7 +203,10 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
req.timeout_ms = 30000;
ncm_http_resp_t resp;
size_t maxb = (u.size > 0 && u.size < (64LL << 20)) ? (size_t)u.size : (64u << 20);
/* 上限: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);
@@ -189,7 +216,7 @@ static int song_local_file(const char *id, char *path, size_t psz, int64_t *byte
if (rc != 0) return rc;
/* 写后检查:新落盘可能把池推过上限 → 再按 LRU 淘汰最旧(刚写的 mtime 最新,存活) */
ncm_cache_evict(&g_st.ctx.cache);
if (ncm_cache_lookup(&g_st.ctx.cache, id, ".mp3", path, psz) != NCM_OK)
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;
@@ -247,12 +274,19 @@ static void run_song(int idx)
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "decode open failed"); return; }
/* 解码分发(§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;
/* 音频输出:解码器输出 S16_LE(minimp3 默认);用 44100/2ch 直开 */
if (audio_out_open(44100, 2, 16) != 0) { mp3dec_ex_close(&dec); set_err(NCM_ERR_IO, "audio open failed"); return; }
/* 音频输出:按实际流参数开(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);
@@ -261,9 +295,11 @@ static void run_song(int idx)
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 dur=%lldms", idx, s.id, (long long)dur);
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);
mp3d_sample_t pcm[PCM_CHUNK];
/* 单块缓冲: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 */
@@ -288,7 +324,7 @@ static void run_song(int idx)
if (!paused_edge) { /* 进暂停:停 PCM 时钟但不丢流(audiod MSG_AUDIO_PAUSE) */
paused_edge = 1;
meow_log(MEOW_LOG_INFO, "play: paused idx=%d pos=%lldms", idx,
(long long)(frames_done * 1000 / 44100));
rate > 0 ? (long long)(frames_done * 1000 / rate) : 0);
audio_out_pause(1);
}
usleep(40 * 1000);
@@ -301,36 +337,32 @@ static void run_song(int idx)
}
if (need_seek_ms >= 0) {
/* 重开并 seek:关闭后重开(字节 offset 近似) */
/* seek:flac 精确帧 / mp3 关+重开按字节近似(decoder 内处理);输出重开 */
meow_log(MEOW_LOG_INFO, "play: seek idx=%d -> %dms", idx, need_seek_ms);
audio_out_close();
mp3dec_ex_close(&dec);
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { set_err(NCM_ERR_IO, "seek reopen failed"); break; }
if (audio_out_open(44100, 2, 16) != 0) { set_err(NCM_ERR_IO, "audio reopen failed"); break; }
int64_t target = (g_file_bytes > 0 && dur > 0)
? (int64_t)g_file_bytes * need_seek_ms / dur : 0;
mp3dec_ex_seek(&dec, (uint64_t)(target > 0 ? target : 0));
frames_done = (int64_t)need_seek_ms * 44100 / 1000;
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;
}
size_t got = mp3dec_ex_read(&dec, pcm, PCM_CHUNK);
if (got == 0) { /* 播完 */
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;
}
int frames = (int)(got / 2);
/* 写失败(流死/audiod 断链):置错停播——旧版静默 break 会被外层当「自然播完」
* 自动顺延下一首,流死时连环跳歌并狂发 song_url 请求(§18 修)。 */
if (audio_out_write(pcm, frames) != frames) {
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 = frames_done * 1000 / 44100;
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;
@@ -338,7 +370,7 @@ static void run_song(int idx)
}
audio_out_close();
mp3dec_ex_close(&dec);
ncm_dec_close(dec);
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
}
+53 -30
View File
@@ -1,13 +1,26 @@
/* test/test_decode.c — 播放解码链路验证(宿主):minimp3 解码 mp3 → audio_out(FILE sink) 写 WAV。
* 不依赖网络/登录(输入本地 mp3)。验证:解码无错、WAV 非空、时长合理、RT 因子。
* 运行:NCM_AUDIO_SINK=file NCM_AUDIO_OUT=/tmp/ncm_decode.wav ./test_decode <path.mp3>
/* test/test_decode.c — 播放解码链路验证(宿主/mipsel+qemu):decoder(magic 分发
* dr_flac/minimp3,§19)→ audio_out(FILE sink) 写 WAV。不依赖网络/登录(输入本地文件)。
* 验证:格式探测正确、rate/ch/bits 按实际流、解码无错、WAV 非空、帧数/时长对、RT 因子。
*
* 运行:NCM_AUDIO_SINK=file NCM_AUDIO_OUT=/tmp/ncm_decode.wav \
* ./test_decode <path> [expect_frames] [expect_rate] [expect_bits]
* expect_* 给定时校验(不一致 exit 1);不给则只要求 frames>0。
*
* 编译(宿主,ncm_app 目录下):
* gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
* -Ivendor/minimp3 -Ivendor/dr_flac \
* app/test/test_decode.c app/play/decoder.c app/play/audio_out.c \
* ../sdk/meow_ui_audio.c ../sdk/meow_ui_log.c -o /tmp/td.o-chain # 见下两行豁免 TU
* gcc -O2 -Ivendor/minimp3 -c app/play/minimp3_impl.c -o /tmp/td_mm.o # vendor 豁免
* gcc -O2 -Ivendor/dr_flac -c app/play/dr_flac_impl.c -o /tmp/td_df.o # vendor 豁免
* (首行改为 -c 逐个或把两个豁免 .o 一并链接:gcc ... -o out/test_decode /tmp/td_mm.o /tmp/td_df.o -lpthread -lm)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../ncm_app_internal.h" /* 引 audio_out / minimp3 声明 */
#include "minimp3_ex.h"
#include "../ncm_app_internal.h" /* audio_out 声明 + 全局态 */
#include "../play/decoder.h"
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER };
@@ -15,6 +28,14 @@ volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
/* SDK meow_ui_log.c 需要(本测试不链 meow_ui_sys.c,同 audiod_chain_sim 口径打桩) */
uint64_t meow_ui_time_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
}
static double now_s(void)
{
struct timespec ts;
@@ -25,43 +46,45 @@ static double now_s(void)
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "vendor/minimp3/vectors/performance/MIPSTest.mp3";
mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { fprintf(stderr, "open failed\n"); return 1; }
int64_t exp_frames = (argc > 2) ? atoll(argv[2]) : -1;
int exp_rate = (argc > 3) ? atoi(argv[3]) : -1;
int exp_bits = (argc > 4) ? atoi(argv[4]) : -1;
static mp3d_sample_t pcm[8192 * 2];
/* 读首帧确定采样率/声道(read 会填 dec.info)*/
size_t got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
int sr = dec.info.hz;
int ch = dec.info.channels;
if (sr <= 0 || ch <= 0) { fprintf(stderr, "bad stream\n"); return 1; }
printf("mp3: %s %dHz x%d detected_samples=%lld bitrate=%dkbps\n",
path, sr, ch, (long long)dec.samples, dec.info.bitrate_kbps);
if (audio_out_open(sr, ch, 16) != 0) { fprintf(stderr, "audio_out open failed\n"); return 1; }
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) {
fprintf(stderr, "open failed\n"); return 1;
}
printf("%s: %s %dHz x%d %dbit total_frames=%lld\n",
path, ncm_dec_format(dec), rate, ch, bits, (long long)total_frames);
if (exp_rate > 0 && rate != exp_rate) { fprintf(stderr, "rate mismatch %d != %d\n", rate, exp_rate); return 1; }
if (exp_bits > 0 && bits != exp_bits) { fprintf(stderr, "bits mismatch %d != %d\n", bits, exp_bits); return 1; }
if (audio_out_open(rate, ch, bits) != 0) { fprintf(stderr, "audio_out open failed\n"); return 1; }
static int32_t pcm[4096 * 2];
long long frames = 0;
double t0 = now_s();
if (got > 0) {
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); return 1; }
frames += fr;
}
for (;;) {
if (g_app_quit) break;
got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
int64_t got = ncm_dec_read(dec, pcm, 4096);
if (got == 0) break;
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); break; }
frames += fr;
if (audio_out_write(pcm, (int)got) != (int)got) { fprintf(stderr, "write failed\n"); break; }
frames += got;
}
double dt = now_s() - t0;
audio_out_close();
mp3dec_ex_close(&dec);
ncm_dec_close(dec);
double secs = (double)frames / (double)sr;
double secs = rate > 0 ? (double)frames / (double)rate : 0;
double rt = (dt > 0) ? secs / dt : 0;
printf("decoded %lld frames = %.2f s audio (%d Hz) in %.3f s => %.1fx RT\n",
frames, secs, sr, dt, rt);
printf("decoded %lld frames = %.3f s audio (%d Hz) in %.3f s => %.1fx RT\n",
frames, secs, rate, dt, rt);
if (exp_frames >= 0 && frames != exp_frames) {
fprintf(stderr, "frames mismatch %lld != %lld\n", frames, (long long)exp_frames);
return 1;
}
printf("OK: %s\n", (frames > 0) ? "decode chain works" : "EMPTY");
return (frames > 0) ? 0 : 1;
}
+70 -15
View File
@@ -14,14 +14,15 @@
*
* 编译(宿主,ncm_app 目录下):
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -c bench/player_flow_sim.c -o /tmp/pf.o
* -Icore -Ivendor/jsmn -c bench/player_flow_sim.c -o /tmp/pf.o
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -c app/play/player_manager.c -o /tmp/pf_pm.o
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -c app/play/player_manager.c -o /tmp/pf_pm.o
* gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o -lpthread -lm
* 运行:./player_flow_sim (0 网络;PASS/FAIL 退出码)
* §19:P6 覆盖缓存键=歌曲ID+档位 + flac type → .flac 落盘扩展名。
*/
#include "../app/ncm_app_internal.h"
#include "minimp3_ex.h"
#include "../app/play/decoder.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@@ -64,6 +65,9 @@ static int g_cache_ok = 1;
static int g_url_rc = NCM_OK;
static int g_dl_delay_ms = 0;
static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */
static const char *g_url_type = "mp3"; /* §19:song_url 注入的 type(mp3/flac) */
static char g_last_key[80]; /* ncm_cache_open 收到的键(档位键断言用) */
static char g_last_ext[8]; /* ncm_cache_open 收到的扩展名 */
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz)
{
@@ -73,7 +77,12 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char
return NCM_OK;
}
int ncm_cache_open(const ncm_cache_t *c, const char *id, const char *ext, ncm_cache_writer_t **w)
{ (void)c; (void)id; (void)ext; *w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK; }
{
(void)c;
snprintf(g_last_key, sizeof g_last_key, "%s", id);
snprintf(g_last_ext, sizeof g_last_ext, "%s", ext ? ext : "");
*w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK;
}
int ncm_cache_write(ncm_cache_writer_t *w, const unsigned char *d, size_t l) { (void)w; (void)d; (void)l; return NCM_OK; }
int ncm_cache_commit(ncm_cache_writer_t *w) { free(w); g_cache_ok = 1; return NCM_OK; }
int ncm_cache_abort(ncm_cache_writer_t *w) { free(w); return NCM_OK; }
@@ -85,7 +94,8 @@ int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int l
(void)dev; (void)ck; (void)id; (void)level;
if (g_url_rc != NCM_OK) return g_url_rc;
memset(out, 0, sizeof *out);
snprintf(out->url, sizeof out->url, "http://stub/%s.mp3", id);
snprintf(out->url, sizeof out->url, "http://stub/%s.%s", id, g_url_type);
snprintf(out->type, sizeof out->type, "%s", g_url_type);
out->size = 4 * 1024 * 1024;
return NCM_OK;
}
@@ -105,22 +115,52 @@ int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx
return NCM_OK;
}
/* ---- 解码器 stub:g_eof 前恒有数据(P1 用 g_reads_left 控制播完点) ---- */
/* ---- 解码器 stub(§19:player_manager 改走 ncm_dec_* 分发层):
* g_eof 前恒有数据(P1 用 g_reads_left 控制播完点) ---- */
static int g_eof;
static int g_reads_left = -1; /* >=0: 剩余读次数后 EOF;-1: 直到 g_eof */
static int g_dec_open_bits = 16; /* 模拟流位宽(记录用) */
int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags)
{ (void)dec; (void)file_name; (void)flags; return 0; }
void mp3dec_ex_close(mp3dec_ex_t *dec) { (void)dec; }
int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position) { (void)dec; (void)position; return 0; }
size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples)
int ncm_dec_open(ncm_decoder_t **out, const char *path, int *rate, int *ch, int *bits,
int64_t *total_frames)
{
(void)dec;
(void)path;
*out = (ncm_decoder_t *)calloc(1, 16);
if (rate) *rate = 44100;
if (ch) *ch = 2;
if (bits) *bits = g_dec_open_bits;
if (total_frames) *total_frames = 44100 * 180;
return *out ? 0 : -1;
}
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames)
{
(void)d;
if (g_eof) return 0;
if (g_reads_left == 0) return 0;
if (g_reads_left > 0) g_reads_left--;
memset(buf, 0, samples * sizeof(mp3d_sample_t));
return samples;
memset(buf, 0, (size_t)cap_frames * 2 * sizeof(int16_t));
return cap_frames;
}
int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t fb, int64_t dur)
{ (void)d; (void)ms; (void)fb; (void)dur; return 0; }
void ncm_dec_close(ncm_decoder_t *d) { free(d); }
const char *ncm_dec_format(const ncm_decoder_t *d) { (void)d; return "stub"; }
/* §19:song_local_file 的缓存键用 ncm_api_level_str(g_level);ext 判定用 ncm_streq */
int ncm_streq(const char *a, const char *b)
{
if (!a || !b) return 0;
return strcmp(a, b) == 0;
}
const char *ncm_api_level_str(int level)
{
switch (level) {
case NCM_LEVEL_HIRES: return "hires";
case NCM_LEVEL_LOSSLESS: return "lossless";
case NCM_LEVEL_EXHIGH: return "exhigh";
case NCM_LEVEL_HIGHER: return "higher";
default: return "standard";
}
}
/* ---- 音频输出 stub:g_write_fail 注入写失败 ---- */
@@ -129,7 +169,7 @@ static int g_writes;
static int g_opens; /* audio_out_open 次数(每首 run_song 开一次) */
int audio_out_open(int rate, int ch, int bits) { (void)rate; (void)ch; (void)bits; g_opens++; return 0; }
int audio_out_write(const short *s, int frames)
int audio_out_write(const void *s, int frames)
{ (void)s; g_writes++; return g_write_fail ? -1 : frames; }
int audio_out_pause(int paused) { (void)paused; return 0; }
void audio_out_close(void) { }
@@ -250,6 +290,21 @@ int main(void)
CHECK(player_manager_error() == 0, "err cleared on retry");
g_eof = 1;
printf("== P6: cache key = id+level, flac type -> .flac ext (§19) ==\n");
usleep(300 * 1000); /* 等 P5 曲收尾 */
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_dl_delay_ms = 0;
g_url_type = "flac";
player_manager_set_quality(NCM_LEVEL_LOSSLESS, 10); /* 换档 → g_level=lossless */
player_manager_play(0);
CHECK(wait_state(NCM_PC_PLAYING, 3000), "flac download song playing");
CHECK(strcmp(g_last_key, "1000_lossless") == 0,
"cache key=%s (expect 1000_lossless = id+档位)", g_last_key);
CHECK(strcmp(g_last_ext, "flac") == 0,
"cache ext=%s (expect flac, type=flac → .flac 落盘)", g_last_ext);
g_url_type = "mp3";
player_manager_set_quality(NCM_LEVEL_STANDARD, 10); /* 还原,避免影响后续(无) */
g_eof = 1;
usleep(200 * 1000);
player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
+5 -4
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# build_ncm.sh — 构建 dev.player.ncm(网易云 App,SDK framework=1):链 meow_ui.a + 核心库 + minimp3 + mbedtls。
# 静态 MIPS32r2 (musl sf)。自有代码(app/ + core/)+ core 支撑 vendor 编译;vendor/minimp3/qrcodegen/jsmn 豁免告警。
# build_ncm.sh — 构建 dev.player.ncm(网易云 App,SDK framework=1):链 meow_ui.a + 核心库 + minimp3 + dr_flac + mbedtls。
# 静态 MIPS32r2 (musl sf)。自有代码(app/ + core/)+ core 支撑 vendor 编译;vendor/minimp3/dr_flac/qrcodegen/jsmn 豁免告警。
# 仅改本 App 目录;编译期不改 SDK(patch_v2/sdk/)、不动 meow_launcherd/。mipsel -Wall -Wextra 0 warning。
set -e
cd "$(dirname "$0")"
@@ -11,7 +11,7 @@ OUT="$PWD/out"
mkdir -p "$OUT/app-mipsel"
INC="-Icore -I../sdk -I../meow_launcherd \
-Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3"
-Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3 -Ivendor/dr_flac"
STRICT="-Wall -Wextra -Os -DJSMN_HEADER"
CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
@@ -21,7 +21,7 @@ APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c"
app/play/ipc_media.c app/play/decoder.c app/play/minimp3_impl.c"
echo "==> app+core (strict)"
for f in $APP $CORE; do
@@ -31,6 +31,7 @@ done
echo "==> vendor impl (warn-exempt)"
$CC -static -c -Os -I../meow_launcherd -Ivendor/minimp3 app/play/minimp3_impl.c -o "$OUT/app-mipsel/minimp3_impl.o"
$CC -static -c -Os -Ivendor/dr_flac app/play/dr_flac_impl.c -o "$OUT/app-mipsel/dr_flac_impl.o"
$CC -static -c -Os -Ivendor/jsmn core/jsmn_impl.c -o "$OUT/app-mipsel/jsmn_impl.o"
$CC -static -c -Os -Ivendor/qrcodegen/c vendor/qrcodegen/c/qrcodegen.c -o "$OUT/app-mipsel/qrcodegen.o"
+4 -2
View File
@@ -14,7 +14,7 @@ MBED="$OUT/mbedtls/host"
INC="-I. -I../sdk -I../meow_launcherd -Icore \
-I$SDK_DIR -I$ROOT/patch_v2/meow_launcherd \
-Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3"
-Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3 -Ivendor/dr_flac"
echo "==> host SDK objects"
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
@@ -33,11 +33,13 @@ APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
app/ui/settings.c app/ui/search.c app/ui/ncm_colors.c app/ui/chrome.c \
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
app/logic/cover.c app/play/player_manager.c app/play/audio_out.c \
app/play/ipc_media.c app/play/minimp3_impl.c"
app/play/ipc_media.c app/play/decoder.c app/play/minimp3_impl.c"
for f in $APP_SRC; do
b=$(basename "$f" .c)
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC "$f" -o "$OUT/host/$b.o" || { echo "app $f FAILED"; exit 1; }
done
# dr_flac 实现 TU:vendor 豁免告警(同 minimp3 口径),放 strict 循环外
$CC_HOST -O2 -c -Ivendor/dr_flac app/play/dr_flac_impl.c -o "$OUT/host/dr_flac_impl.o" || { echo "dr_flac_impl FAILED"; exit 1; }
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -DJSMN_HEADER $INC app/test/test_ncm_app.c -o "$OUT/host/app_test.o" || exit 1
echo "==> link ncm_host"
+10 -3
View File
@@ -267,12 +267,19 @@ int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char
ncm_play_url_t p;
int rc = ncm_api_song_url(device_id, cookie, id, lv, &p);
if (rc != NCM_OK) continue;
/* 只接受有 url 且 type 为 mp3(v1 不解 flac) */
if (p.url[0] && (ncm_streq(p.type, "mp3") || p.type[0] == 0)) {
/* §19:接受 mp3 与 flac(dr_flac 落地后无损/Hi-Res 真实生效)。
* type 权威;type 缺失(旧端点兼容)时看 url 扩展名,默认当 mp3 收。 */
int ok = ncm_streq(p.type, "mp3") || ncm_streq(p.type, "flac");
if (!p.type[0]) {
ok = 1;
const char *dot = strrchr(p.url, '.');
if (dot && ncm_streq(dot, ".flac")) ncm_strlcpy(p.type, "flac", sizeof p.type);
}
if (p.url[0] && ok) {
*out = p;
return NCM_OK;
}
/* url 为 null 或 flac:继续降级 */
/* url 为 null 或未知格式:继续降级 */
}
return NCM_ERR_NOTFOUND;
}
+4 -3
View File
@@ -15,7 +15,8 @@
extern "C" {
#endif
/* 音质档位(song_url_v1 level)。v1 可解码 mp3:standard/higher/exhigh。 */
/* 音质档位(song_url_v1 level)。可解码 mp3 + flac(§19 dr_flac 落地):
* standard/higher/exhigh 通常 mp3;lossless/hires 通常 flac(取决于曲库与会员)。 */
enum {
NCM_LEVEL_STANDARD = 0,
NCM_LEVEL_HIGHER,
@@ -75,8 +76,8 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t
int ncm_api_song_url(const char *device_id, const char *cookie, const char *id,
int level, ncm_play_url_t *out);
/* 播放地址自动降级:从 desired 档开始,遇 url 为 null 或非 mp3(本机不可解码)则逐步降档,
* 只返回 mp3。desired 默认 hires。 */
/* 播放地址自动降级:从 desired 档开始,遇 url 为 null 或未知格式则逐步降档。
* §19 起接受 mp3 与 flac(type 权威;type 缺失看 url 扩展名,默认 mp3)。desired 默认 hires。 */
int ncm_api_song_url_auto(const char *device_id, const char *cookie, const char *id,
int desired, ncm_play_url_t *out);
+12 -2
View File
@@ -63,9 +63,12 @@ const char *ncm_cache_dir(const ncm_cache_t *c) {
return c ? c->dir : "";
}
/* 路径:song_<id>.<ext>。§19 正名:ext 剥离至多一个前导点——旧版调用方传 ".mp3"
* 会生成双点文件名 `song_<id>..mp3`(历史存量),新版一律单点。 */
int ncm_cache_path(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz) {
if (!c || !id || !out) return NCM_ERR_ARG;
if (ext && ext[0] == '.') ext++;
int n = snprintf(out, out_sz, "%s/song_%s.%s", c->dir, id, ext ? ext : "bin");
return (n >= 0 && (size_t)n < out_sz) ? NCM_OK : NCM_ERR_FULL;
}
@@ -75,8 +78,15 @@ int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
char path[NCM_CACHE_DIR_MAX + 32];
if (ncm_cache_path(c, id, ext, path, sizeof path) != 0) return NCM_ERR_FULL;
struct stat st;
if (stat(path, &st) != 0) return NCM_ERR_NOENT;
if (!S_ISREG(st.st_mode)) return NCM_ERR_NOENT;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
/* §19 温和兼容:历史双点名 `song_<id>..<ext>`(旧版 ext 带前导点所致)。
* 命中照常刷新 LRU;不主动改名/删除,由 LRU 自然淘汰。 */
const char *e = ext;
if (e && e[0] == '.') e++;
int n = snprintf(path, sizeof path, "%s/song_%s..%s", c->dir, id, e ? e : "bin");
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;
}
utime(path, NULL); /* 刷新最后访问(按 mtime LRU) */
ncm_strlcpy(out, path, out_sz);
return NCM_OK;
+2 -1
View File
@@ -38,7 +38,8 @@ const char *ncm_cache_dir(const ncm_cache_t *c);
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。 */
/* 查找已缓存文件。命中:写 out 路径并刷新 mtime,返回 NCM_OK。未命中:NCM_ERR_NOENT。
* §19:ext 前导点被忽略(新旧调用方同路径);单点 miss 时回退历史双点名 `song_<id>..<ext>`。 */
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext,
char *out, size_t out_sz);
Binary file not shown.
Binary file not shown.
+15
View File
@@ -142,6 +142,21 @@ static void test_cache(void) {
/* 恢复默认上限 */
ncm_cache_set_limit_gb(&c, 10);
CHECK(ncm_cache_limit_gb(&c) == 10, "limit_gb reset to 10");
/* §19 命名正名:ext 带前导点(".flac")与不带("flac")生成同一单点路径 */
char p2[NCM_CACHE_DIR_MAX + 32];
CHECKRC(ncm_cache_path(&c, "103", ".flac", p2, sizeof p2), "path with dotted ext");
CHECK(strstr(p2, "song_103.flac") != NULL && strstr(p2, "..") == NULL,
"dotted ext normalized to single dot (%s)", p2);
/* §19 温和兼容:历史双点名 song_<id>..mp3(旧版 ext 带前导点所致)lookup 可命中 */
{
char legacy[NCM_CACHE_DIR_MAX + 40];
snprintf(legacy, sizeof legacy, "%s/song_104..mp3", dir);
FILE *lf = fopen(legacy, "wb");
if (lf) { fputs("LEGACY", lf); fclose(lf); }
int lrc = ncm_cache_lookup(&c, "104", "mp3", path, sizeof path);
CHECK(lrc == NCM_OK && strstr(path, "song_104..mp3") != NULL,
"legacy double-dot name hit (rc=%d path=%s)", lrc, path);
}
}
static void test_qr(void) {
Binary file not shown.
File diff suppressed because it is too large. Load diff