feat(ncm): 缓存文件内嵌元数据模块 ncm_tag(FLAC VORBIS_COMMENT / MP3 ID3v2.3 写读 + 音频头探测)
core/ncm_tag.c/.h(纯 libc,宿主可单测,不依赖 mbedtls/jsmn): - FLAC:在元数据末块之后插入 VORBIS_COMMENT(TITLE/ARTIST/ALBUM),旧 VORBIS_COMMENT 替换、PADDING/PICTURE 原样保留;MP3:ID3v2.3 TIT2/TPE1/TALB(UTF-16LE+BOM,中英文 与 emoji 均可),源已带 ID3v2 时先剥旧标签(不留双标签)。 - **按实际容器分支**(读 fLaC 魔数):缓存名档位段是请求档,真机存在 song_<id>_hires.mp3(无损请求被降级链落到 exhigh mp3),不能按名字判容器。 - 写入 = 同目录 <path>.tag.tmp + 64KB 分块搬运 + fsync + rename 原子替换;任一步失败 即 unlink 临时件、原文件保持不变(调用方 commit 裸流降级),峰值内存 64KB。 - ncm_audio_probe_file:只读文件头探测实际容器/位深/码率(FLAC STREAMINFO / MP3 首帧), 「已缓存列表」据此显示实际档位而非请求档。 - test/test_ncm_tag.c + build_ncm_tag_host.sh:FLAC/MP3 构建+读回、中文 roundtrip、 旧标签替换、容器判定、3MB 分块、失败降级、探测 —— 51 断言全过,0 warning。
This commit is contained in:
1 parent
e56c837f4f
commit
184ebd0ac9
7 files changed
+1276
-3
No files matched your search
@@ -0,0 +1,462 @@
|
||||
/* test/test_ncm_tag.c — 缓存文件内元数据嵌入宿主单测(纯 libc,不依赖 mbedtls/jsmn)。
|
||||
*
|
||||
* 覆盖(NCM_TAG_CHUNK=64KB 分块重写):
|
||||
* ① FLAC:无 tag / 已有 VORBIS_COMMENT + PADDING 两种源 → 嵌入后块结构合法、
|
||||
* VORBIS_COMMENT 唯一位于元数据末块、三项键值可读回、音频帧字节逐字节不变;
|
||||
* ② MP3:裸流 / 已带 ID3v2.3 旧标签 两种源 → 只留一个 ID3v2.3 头、TIT2/TPE1/TALB 可读回、
|
||||
* 旧标签被替换、音频字节不变;
|
||||
* ③ 中文标题(UTF-8 → UTF-16LE → UTF-8)roundtrip;空元数据不动文件;
|
||||
* ④ 大文件(3MB)分块搬运:音频段逐字节一致(峰值内存=64KB,见 ncm_tag.c 头注释);
|
||||
* ⑤ 失败降级:.flac 扩展名但 magic 不符 / 文件不存在 → 返回错误、原文件不变、无临时残体。
|
||||
*
|
||||
* 编译/运行:./build_ncm_tag_host.sh && ./test/test_ncm_tag_host
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ncm_common.h"
|
||||
#include "ncm_tag.h"
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, fmt, ...) do { \
|
||||
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||||
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||||
} while (0)
|
||||
|
||||
/* 相对 CWD 的测试目录:main 先 chdir 到 $TMPDIR(Termux 的 /tmp 不可写),再建本目录。 */
|
||||
#define TMPDIR_PREFIX "ncm_tag_test"
|
||||
|
||||
static void mkdir_tmp(void)
|
||||
{
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s; mkdir -p %s", TMPDIR_PREFIX, TMPDIR_PREFIX);
|
||||
if (system(cmd) != 0) { /* best-effort */ }
|
||||
}
|
||||
|
||||
static long file_size(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(p, &st) != 0) return -1;
|
||||
return (long)st.st_size;
|
||||
}
|
||||
|
||||
static unsigned char *file_slurp(const char *p, size_t *len_out)
|
||||
{
|
||||
FILE *f = fopen(p, "rb");
|
||||
unsigned char *buf;
|
||||
long n;
|
||||
if (!f) return NULL;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
||||
n = ftell(f);
|
||||
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; }
|
||||
buf = (unsigned char *)malloc((size_t)n ? (size_t)n : 1);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
if (n > 0 && fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; }
|
||||
fclose(f);
|
||||
*len_out = (size_t)n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int file_put(const char *p, const void *buf, size_t len)
|
||||
{
|
||||
FILE *f = fopen(p, "wb");
|
||||
if (!f) return -1;
|
||||
if (len && fwrite(buf, 1, len, f) != len) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int file_append(const char *p, const void *buf, size_t len)
|
||||
{
|
||||
FILE *f = fopen(p, "ab");
|
||||
if (!f) return -1;
|
||||
if (len && fwrite(buf, 1, len, f) != len) { fclose(f); return -1; }
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 逐字节比较两段内存是否相同;返回首个不同位置(-1=全同) */
|
||||
static long mem_diff(const unsigned char *a, const unsigned char *b, size_t n)
|
||||
{
|
||||
for (size_t i = 0; i < n; i++) if (a[i] != b[i]) return (long)i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---- 样本构造:最小合法 FLAC(STREAMINFO 34B + 可选附加块 + 音频帧) ---- */
|
||||
static void flac_streaminfo(unsigned char out[34], int bps)
|
||||
{
|
||||
memset(out, 0, 34);
|
||||
out[0] = 0x10; out[1] = 0x00; /* min block 4096 */
|
||||
out[2] = 0x10; out[3] = 0x00; /* max block 4096 */
|
||||
/* 8 字节:sample_rate 44100 / ch 2 / bps / total_samples 44100 */
|
||||
out[10] = 0x0A; out[11] = 0xC4;
|
||||
out[12] = (unsigned char)(0x40 | 0x02 | (((bps - 1) >> 4) & 0x01));
|
||||
out[13] = (unsigned char)(((bps - 1) & 0x0F) << 4);
|
||||
out[14] = 0x00; out[15] = 0x00;
|
||||
out[16] = 0xAC; out[17] = 0x44; /* total samples 44100 */
|
||||
}
|
||||
|
||||
static int flac_make(const char *path, int with_old_tag, size_t audio_len, unsigned char seed,
|
||||
int bps)
|
||||
{
|
||||
unsigned char si[34];
|
||||
unsigned char hdr[4];
|
||||
flac_streaminfo(si, bps);
|
||||
file_put(path, "fLaC", 4);
|
||||
hdr[0] = (with_old_tag ? 0x00 : 0x80); /* last = 无附加块 */
|
||||
hdr[1] = 0x00; hdr[2] = 0x00; hdr[3] = 34;
|
||||
file_append(path, hdr, 4);
|
||||
file_append(path, si, 34);
|
||||
if (with_old_tag) {
|
||||
/* 旧 VORBIS_COMMENT(应被替换) */
|
||||
static const char old_c[] = "TITLE=old";
|
||||
unsigned char blk[64];
|
||||
size_t vlen = 9, plen = 4 + vlen + 4 + 4 + sizeof(old_c) - 1;
|
||||
memset(blk, 0, sizeof blk);
|
||||
blk[0] = 0x04; /* type 4, 非 last */
|
||||
blk[1] = 0; blk[2] = 0; blk[3] = (unsigned char)plen;
|
||||
blk[4] = (unsigned char)vlen; /* vendor_len LE */
|
||||
memcpy(blk + 8, "MeowOSold", vlen);
|
||||
blk[8 + vlen] = 1; /* count = 1 */
|
||||
blk[12 + vlen] = (unsigned char)(sizeof(old_c) - 1);
|
||||
memcpy(blk + 16 + vlen, old_c, sizeof(old_c) - 1);
|
||||
file_append(path, blk, 4 + plen);
|
||||
/* PADDING(type 1,last) */
|
||||
blk[0] = 0x81; blk[1] = 0; blk[2] = 0; blk[3] = 8;
|
||||
file_append(path, blk, 4);
|
||||
file_append(path, " ", 8);
|
||||
}
|
||||
/* 假音频帧(内容只需逐字节不变) */
|
||||
for (size_t i = 0; i < audio_len; i++) {
|
||||
unsigned char b = (unsigned char)(seed + (unsigned char)(i * 31u));
|
||||
if (file_append(path, &b, 1) != 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 扫描 FLAC 块表:返回 VORBIS_COMMENT 的 payload 偏移/长度、音频帧起始;失败返回 -1 */
|
||||
static int flac_scan(const unsigned char *buf, size_t len, size_t *vc_off, size_t *vc_len,
|
||||
size_t *audio_off, int *n_vc, int *last_ok)
|
||||
{
|
||||
size_t off = 4;
|
||||
int nvc = 0;
|
||||
if (len < 8 || memcmp(buf, "fLaC", 4) != 0) return -1;
|
||||
*vc_off = 0; *vc_len = 0; *n_vc = 0; *last_ok = 0;
|
||||
for (int guard = 0; guard < 64; guard++) {
|
||||
unsigned char h;
|
||||
size_t bl;
|
||||
if (off + 4 > len) return -1;
|
||||
h = buf[off];
|
||||
bl = ((size_t)buf[off + 1] << 16) | ((size_t)buf[off + 2] << 8) | buf[off + 3];
|
||||
off += 4;
|
||||
if (off + bl > len) return -1;
|
||||
if ((h & 0x7f) == 4) { nvc++; *vc_off = off; *vc_len = bl; }
|
||||
off += bl;
|
||||
if (h & 0x80) {
|
||||
*last_ok = ((h & 0x7f) == 4) ? 1 : 0;
|
||||
*audio_off = off;
|
||||
*n_vc = nvc;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void test_flac(void)
|
||||
{
|
||||
ncm_tag_meta_t m, got;
|
||||
char path[256];
|
||||
unsigned char *before, *after;
|
||||
size_t blen, alen, vc_off, vc_len, audio_off;
|
||||
int nvc = 0, last_ok = 0;
|
||||
|
||||
printf("== FLAC VORBIS_COMMENT ==\n");
|
||||
memset(&m, 0, sizeof m);
|
||||
snprintf(m.title, sizeof m.title, "夜曲");
|
||||
snprintf(m.artist, sizeof m.artist, "周杰伦");
|
||||
snprintf(m.album, sizeof m.album, "十一月的萧邦");
|
||||
|
||||
/* ① 裸 FLAC(无旧 tag) */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/a.flac");
|
||||
CHECK(flac_make(path, 0, 1024, 7, 16) == 0, "build minimal flac");
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(ncm_tag_embed_file(path, "flac", &m) == 0, "embed flac (no old tag)");
|
||||
after = file_slurp(path, &alen);
|
||||
CHECK(before && after && alen > blen, "file grew by tag block (%zu -> %zu)", blen, alen);
|
||||
CHECK(flac_scan(after, alen, &vc_off, &vc_len, &audio_off, &nvc, &last_ok) == 0,
|
||||
"metadata block table parses");
|
||||
CHECK(nvc == 1 && last_ok == 1, "exactly one VORBIS_COMMENT, last block (n=%d last=%d)",
|
||||
nvc, last_ok);
|
||||
CHECK(alen - audio_off == 1024 &&
|
||||
mem_diff(after + audio_off, before + blen - 1024, 1024) < 0,
|
||||
"audio frames preserved byte-for-byte");
|
||||
memset(&got, 0, sizeof got);
|
||||
CHECK(ncm_tag_read_file(path, &got) == 0, "read back embedded tag");
|
||||
CHECK(strcmp(got.title, "夜曲") == 0, "title roundtrip UTF-8 (got: %s)", got.title);
|
||||
CHECK(strcmp(got.artist, "周杰伦") == 0, "artist roundtrip (got: %s)", got.artist);
|
||||
CHECK(strcmp(got.album, "十一月的萧邦") == 0, "album roundtrip (got: %s)", got.album);
|
||||
free(before); free(after);
|
||||
|
||||
/* ② 已有旧 VORBIS_COMMENT + PADDING:旧块被剔除,只留我们的,PADDING 保留 */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/b.flac");
|
||||
CHECK(flac_make(path, 1, 2048, 11, 16) == 0, "build flac with old tag + padding");
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(ncm_tag_embed_file(path, "flac", &m) == 0, "embed flac (old tag)");
|
||||
after = file_slurp(path, &alen);
|
||||
CHECK(flac_scan(after, alen, &vc_off, &vc_len, &audio_off, &nvc, &last_ok) == 0 &&
|
||||
nvc == 1 && last_ok == 1,
|
||||
"old VORBIS_COMMENT replaced (n=%d)", nvc);
|
||||
CHECK(alen - audio_off == 2048 &&
|
||||
mem_diff(after + audio_off, before + blen - 2048, 2048) < 0,
|
||||
"audio frames preserved (with old tag source)");
|
||||
memset(&got, 0, sizeof got);
|
||||
CHECK(ncm_tag_read_file(path, &got) == 0 && strcmp(got.title, "夜曲") == 0,
|
||||
"read back new title (got: %s)", got.title);
|
||||
/* 旧 title "old" 不应残留 */
|
||||
{
|
||||
int has_old = 0;
|
||||
for (size_t i = 0; i + 3 <= vc_len; i++)
|
||||
if (memcmp(after + vc_off + i, "old", 3) == 0) has_old = 1;
|
||||
CHECK(!has_old, "old tag value removed from VORBIS_COMMENT");
|
||||
}
|
||||
free(before); free(after);
|
||||
}
|
||||
|
||||
static void test_mp3(void)
|
||||
{
|
||||
ncm_tag_meta_t m, got;
|
||||
char path[256];
|
||||
unsigned char *before, *after;
|
||||
size_t blen, alen;
|
||||
size_t audio_before = 0, audio_after;
|
||||
|
||||
printf("== MP3 ID3v2.3 ==\n");
|
||||
memset(&m, 0, sizeof m);
|
||||
snprintf(m.title, sizeof m.title, "晴天 (Live)");
|
||||
snprintf(m.artist, sizeof m.artist, "Jay Chou");
|
||||
snprintf(m.album, sizeof m.album, "叶惠美");
|
||||
|
||||
/* ① 裸 MP3 流(无 ID3) */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/a.mp3");
|
||||
{
|
||||
unsigned char frame[512];
|
||||
for (size_t i = 0; i < sizeof frame; i++) frame[i] = (unsigned char)(0xF0 ^ (i * 7));
|
||||
frame[0] = 0xFF; frame[1] = 0xFB; /* MPEG1 Layer III */
|
||||
frame[2] = 0xE0; frame[3] = 0x00; /* 320kbps / 44.1kHz */
|
||||
file_put(path, frame, sizeof frame);
|
||||
}
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(ncm_tag_embed_file(path, "mp3", &m) == 0, "embed mp3 (raw stream)");
|
||||
after = file_slurp(path, &alen);
|
||||
CHECK(before && after && alen > blen, "file grew by ID3v2.3 header (%zu -> %zu)", blen, alen);
|
||||
CHECK(alen - blen >= 10 && memcmp(after, "ID3", 3) == 0 && after[3] == 3,
|
||||
"starts with ID3v2.3 header");
|
||||
{
|
||||
size_t tsize = ((size_t)(after[6] & 0x7f) << 21) | ((size_t)(after[7] & 0x7f) << 14) |
|
||||
((size_t)(after[8] & 0x7f) << 7) | (size_t)(after[9] & 0x7f);
|
||||
audio_after = 10 + tsize;
|
||||
CHECK(audio_after == alen - blen, "ID3 size field matches appended header (%zu)",
|
||||
audio_after);
|
||||
}
|
||||
CHECK(mem_diff(after + audio_after, before, blen) < 0, "audio bytes preserved (raw source)");
|
||||
memset(&got, 0, sizeof got);
|
||||
CHECK(ncm_tag_read_file(path, &got) == 0, "read back embedded mp3 tag");
|
||||
CHECK(strcmp(got.title, "晴天 (Live)") == 0, "title roundtrip (got: %s)", got.title);
|
||||
CHECK(strcmp(got.artist, "Jay Chou") == 0, "artist roundtrip (got: %s)", got.artist);
|
||||
CHECK(strcmp(got.album, "叶惠美") == 0, "album roundtrip (got: %s)", got.album);
|
||||
free(before); free(after);
|
||||
|
||||
/* ② 已带 ID3v2.3 旧标签:旧标签被跳过(不留双标签),音频段不变 */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/b.mp3");
|
||||
{
|
||||
unsigned char old[64];
|
||||
unsigned char frame[256];
|
||||
size_t body = 10 + 1 + 2 + 2 * 3; /* TIT2 "old" UTF-16LE */
|
||||
memset(old, 0, sizeof old);
|
||||
memcpy(old, "ID3", 3); old[3] = 3;
|
||||
old[6] = (unsigned char)((body >> 21) & 0x7f);
|
||||
old[7] = (unsigned char)((body >> 14) & 0x7f);
|
||||
old[8] = (unsigned char)((body >> 7) & 0x7f);
|
||||
old[9] = (unsigned char)(body & 0x7f);
|
||||
memcpy(old + 10, "TIT2", 4);
|
||||
old[14] = 0; old[15] = 0; old[16] = 0; old[17] = 1 + 2 + 6;
|
||||
old[18] = 0; old[19] = 0;
|
||||
old[20] = 1; old[21] = 0xFF; old[22] = 0xFE;
|
||||
old[23] = 'o'; old[24] = 0; old[25] = 'l'; old[26] = 0; old[27] = 'd'; old[28] = 0;
|
||||
for (size_t i = 0; i < sizeof frame; i++) frame[i] = (unsigned char)(i * 13u + 3u);
|
||||
frame[0] = 0xFF; frame[1] = 0xFB;
|
||||
frame[2] = 0xE0; frame[3] = 0x00; /* 320kbps / 44.1kHz */
|
||||
file_put(path, old, 10 + body);
|
||||
file_append(path, frame, sizeof frame);
|
||||
audio_before = 10 + body;
|
||||
}
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(ncm_tag_embed_file(path, "mp3", &m) == 0, "embed mp3 (existing old tag)");
|
||||
after = file_slurp(path, &alen);
|
||||
{
|
||||
size_t tsize = ((size_t)(after[6] & 0x7f) << 21) | ((size_t)(after[7] & 0x7f) << 14) |
|
||||
((size_t)(after[8] & 0x7f) << 7) | (size_t)(after[9] & 0x7f);
|
||||
audio_after = 10 + tsize;
|
||||
CHECK(alen - audio_after == blen - audio_before &&
|
||||
mem_diff(after + audio_after, before + audio_before, blen - audio_before) < 0,
|
||||
"audio preserved, old ID3 stripped (audio_off %zu)", audio_after);
|
||||
}
|
||||
memset(&got, 0, sizeof got);
|
||||
CHECK(ncm_tag_read_file(path, &got) == 0 && strcmp(got.title, "晴天 (Live)") == 0,
|
||||
"read back after replacing old tag (got: %s)", got.title);
|
||||
free(before); free(after);
|
||||
}
|
||||
|
||||
static void test_edges(void)
|
||||
{
|
||||
ncm_tag_meta_t m, empty;
|
||||
char path[256];
|
||||
unsigned char *before, *after;
|
||||
size_t blen, alen;
|
||||
long before_sz;
|
||||
|
||||
printf("== 边界 / 降级 ==\n");
|
||||
/* 空元数据:不动文件 */
|
||||
memset(&empty, 0, sizeof empty);
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/noop.flac");
|
||||
flac_make(path, 0, 64, 1, 16);
|
||||
before_sz = file_size(path);
|
||||
CHECK(ncm_tag_embed_file(path, "flac", &empty) == 0, "empty meta returns OK");
|
||||
CHECK(file_size(path) == before_sz, "empty meta leaves file untouched");
|
||||
|
||||
/* 无 tag 的裸文件:读取返回 NOENT */
|
||||
memset(&m, 0, sizeof m);
|
||||
CHECK(ncm_tag_read_file(path, &m) == NCM_ERR_NOENT, "untagged file -> NOENT");
|
||||
|
||||
/* 扩展名是 flac 但 magic 不符:失败且原文件不变、无临时残体
|
||||
* (容器以 magic 为准:声明 flac 却无 fLaC 魔数 = 残体/损坏,拒绝写 tag 走降级) */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/fake.flac");
|
||||
file_put(path, "NOTFLAC-DATA", 12);
|
||||
memset(&m, 0, sizeof m);
|
||||
snprintf(m.title, sizeof m.title, "x");
|
||||
CHECK(ncm_tag_embed_file(path, "flac", &m) != 0, "non-FLAC magic rejected");
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(blen == 12 && memcmp(before, "NOTFLAC-DATA", 12) == 0, "original file unchanged");
|
||||
free(before);
|
||||
{
|
||||
struct stat st;
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof tmp, "%s.tag.tmp", path);
|
||||
CHECK(stat(tmp, &st) != 0, "no .tag.tmp left behind");
|
||||
}
|
||||
/* 不存在的文件 */
|
||||
CHECK(ncm_tag_embed_file(TMPDIR_PREFIX "/nope.mp3", "mp3", &m) != 0, "missing file -> error");
|
||||
CHECK(ncm_tag_read_file(TMPDIR_PREFIX "/nope.mp3", &m) == NCM_ERR_NOENT,
|
||||
"read missing -> NOENT");
|
||||
|
||||
/* 大文件(3MB)分块搬运:音频段逐字节一致(对应 NCM_TAG_CHUNK 多次循环) */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/big.mp3");
|
||||
{
|
||||
size_t n = 3u * 1024u * 1024u;
|
||||
unsigned char *big = (unsigned char *)malloc(n);
|
||||
CHECK(big != NULL, "alloc 3MB fixture");
|
||||
if (!big) { printf("\n== RESULT: FAIL ==\n"); exit(1); }
|
||||
for (size_t i = 0; i < n; i++) big[i] = (unsigned char)(i * 131u + (i >> 8));
|
||||
file_put(path, big, n);
|
||||
memset(&m, 0, sizeof m);
|
||||
snprintf(m.title, sizeof m.title, "big");
|
||||
CHECK(ncm_tag_embed_file(path, "mp3", &m) == 0, "embed 3MB (chunked copy)");
|
||||
after = file_slurp(path, &alen);
|
||||
if (after) {
|
||||
size_t tsize = ((size_t)(after[6] & 0x7f) << 21) | ((size_t)(after[7] & 0x7f) << 14) |
|
||||
((size_t)(after[8] & 0x7f) << 7) | (size_t)(after[9] & 0x7f);
|
||||
size_t audio_after = 10 + tsize;
|
||||
CHECK(alen - audio_after == n && mem_diff(after + audio_after, big, n) < 0,
|
||||
"3MB audio preserved across chunk boundaries");
|
||||
} else {
|
||||
CHECK(0, "read back 3MB file");
|
||||
}
|
||||
free(after);
|
||||
free(big);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_container_and_probe(void)
|
||||
{
|
||||
ncm_tag_meta_t m, got;
|
||||
ncm_audio_info_t ai;
|
||||
char path[256];
|
||||
unsigned char *before, *after;
|
||||
size_t blen, alen, vc_off, vc_len, audio_off;
|
||||
int nvc = 0, last_ok = 0;
|
||||
|
||||
printf("== 实际容器分支 + 音频头探测 ==\n");
|
||||
memset(&m, 0, sizeof m);
|
||||
snprintf(m.title, sizeof m.title, "Container");
|
||||
snprintf(m.artist, sizeof m.artist, "A");
|
||||
|
||||
/* 同名档位/扩展名不可信(真机 song_<id>_hires.mp3 实证):内容 fLaC + ext mp3
|
||||
* → 必须写 VORBIS_COMMENT(按容器),不能塞 ID3。 */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/misnamed.mp3");
|
||||
flac_make(path, 0, 512, 3, 24);
|
||||
before = file_slurp(path, &blen);
|
||||
CHECK(ncm_tag_embed_file(path, "mp3", &m) == 0, "embed with ext=mp3 but flac content");
|
||||
after = file_slurp(path, &alen);
|
||||
CHECK(alen >= 4 && memcmp(after, "fLaC", 4) == 0, "container decides: still fLaC (no ID3)");
|
||||
CHECK(flac_scan(after, alen, &vc_off, &vc_len, &audio_off, &nvc, &last_ok) == 0 &&
|
||||
nvc == 1 && last_ok == 1, "VORBIS_COMMENT written by actual container (n=%d)", nvc);
|
||||
CHECK(alen - audio_off == 512 &&
|
||||
mem_diff(after + audio_off, before + blen - 512, 512) < 0, "audio preserved");
|
||||
memset(&got, 0, sizeof got);
|
||||
CHECK(ncm_tag_read_file(path, &got) == 0 && strcmp(got.title, "Container") == 0,
|
||||
"read back by container (got: %s)", got.title);
|
||||
free(before); free(after);
|
||||
|
||||
/* 探测:FLAC 16bit → SQ 依据;24bit → Hi-Res 依据 */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/p16.flac");
|
||||
flac_make(path, 0, 64, 1, 16);
|
||||
memset(&ai, 0, sizeof ai);
|
||||
CHECK(ncm_audio_probe_file(path, &ai) == 0 && strcmp(ai.container, "flac") == 0 &&
|
||||
ai.bits_per_sample == 16 && ai.sample_rate == 44100,
|
||||
"probe flac 16bit: %s %dbit %dHz", ai.container, ai.bits_per_sample, ai.sample_rate);
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/p24.flac");
|
||||
flac_make(path, 0, 64, 1, 24);
|
||||
memset(&ai, 0, sizeof ai);
|
||||
CHECK(ncm_audio_probe_file(path, &ai) == 0 && ai.bits_per_sample == 24,
|
||||
"probe flac 24bit: %dbit %dHz", ai.bits_per_sample, ai.sample_rate);
|
||||
|
||||
/* 探测:MP3 原始帧 / 带 ID3 头(嵌入后)都应识别出 mp3 + 320kbps */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/p.mp3");
|
||||
{
|
||||
unsigned char frame[512];
|
||||
for (size_t i = 0; i < sizeof frame; i++) frame[i] = (unsigned char)(0xF0 ^ (i * 7));
|
||||
frame[0] = 0xFF; frame[1] = 0xFB; frame[2] = 0xE0; frame[3] = 0x00;
|
||||
file_put(path, frame, sizeof frame);
|
||||
}
|
||||
memset(&ai, 0, sizeof ai);
|
||||
CHECK(ncm_audio_probe_file(path, &ai) == 0 && strcmp(ai.container, "mp3") == 0 &&
|
||||
ai.bitrate_kbps == 320 && ai.sample_rate == 44100,
|
||||
"probe raw mp3: %d kbps %d Hz", ai.bitrate_kbps, ai.sample_rate);
|
||||
CHECK(ncm_tag_embed_file(path, "mp3", &m) == 0, "tag raw mp3 for probe");
|
||||
memset(&ai, 0, sizeof ai);
|
||||
CHECK(ncm_audio_probe_file(path, &ai) == 0 && ai.bitrate_kbps == 320,
|
||||
"probe tagged mp3 skips ID3v2 (%d kbps)", ai.bitrate_kbps);
|
||||
|
||||
/* 未知容器 */
|
||||
snprintf(path, sizeof path, TMPDIR_PREFIX "/p.bin");
|
||||
file_put(path, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b", 12);
|
||||
memset(&ai, 0, sizeof ai);
|
||||
CHECK(ncm_audio_probe_file(path, &ai) != 0, "garbage -> probe fails (unknown container)");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR"); /* Termux:/tmp 不可写,用 $TMPDIR */
|
||||
printf("ncm_tag self-test\n");
|
||||
if (base && base[0] && chdir(base) != 0) printf(" warn: chdir(%s) failed\n", base);
|
||||
mkdir_tmp();
|
||||
test_flac();
|
||||
test_mp3();
|
||||
test_edges();
|
||||
test_container_and_probe();
|
||||
printf("\n%s\n", g_fail ? "== RESULT: FAIL ==" : "== RESULT: PASS ==");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user