From 42718385cc2534708671251c3e44a89952dc3f73 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 3 Sep 2026 14:40:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E6=89=B91=20meow=5Flyric=20?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=AD=8C=E8=AF=8D=E7=BB=84=E4=BB=B6=20?= =?UTF-8?q?=E2=80=94=20lrc/tlrc=E8=A7=A3=E6=9E=90+=E5=AD=98=E5=8F=96+?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2+=E6=8A=98=E8=A1=8C=E8=BE=85=E5=8A=A9,?= =?UTF-8?q?=E7=BA=AFlibc+pthread=E9=9B=B6=E4=BE=9D=E8=B5=96,=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB54=E6=96=AD=E8=A8=80=E5=85=A8=E8=BF=87,=E6=9A=82?= =?UTF-8?q?=E4=B8=8D=E5=B9=B6=E5=85=A5meow=5Fui.a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch_v2/sdk/meow_lyric.c | 314 +++++++++++++++++++++++++++++++++ patch_v2/sdk/meow_lyric.h | 82 +++++++++ patch_v2/sdk/test_meow_lyric | Bin 0 -> 72448 bytes patch_v2/sdk/test_meow_lyric.c | 205 +++++++++++++++++++++ 4 files changed, 601 insertions(+) create mode 100644 patch_v2/sdk/meow_lyric.c create mode 100644 patch_v2/sdk/meow_lyric.h create mode 100755 patch_v2/sdk/test_meow_lyric create mode 100644 patch_v2/sdk/test_meow_lyric.c diff --git a/patch_v2/sdk/meow_lyric.c b/patch_v2/sdk/meow_lyric.c new file mode 100644 index 0000000..de7e779 --- /dev/null +++ b/patch_v2/sdk/meow_lyric.c @@ -0,0 +1,314 @@ +/* + * meow_lyric.c — SDK 通用歌词组件实现(见 meow_lyric.h 的契约说明)。 + * + * 本 TU 为 ncm_app/app/logic/lyric.c(解析/清空/查询)+ player.c 的 lyr_wrap_lines(折行)的 + * 通用化上收:去掉 ncm 特有命名(NCM_LYRIC_LINES / LRC_MAX_TEXT / lyrical 前缀),改用 + * meow_lyric_* 前缀与 SDK 命名;行上限由编译期宏变为「静态硬上限 + 运行时可配置」; + * 时间戳存储用 int64_t(跨 32/64 位宿主一致);宽测量由使用方注入回调(不解耦渲染层)。 + * 逻辑(LRC 时间标签解析、时间升序排序、翻译按 timestamp 合并、字面 \n 当换行、 + * 流式有界、超宽折行中西文混排)与 ncm 逐行保持一致;改动仅限命名/接口/上限可配。 + * + * 线程安全:单把 pthread 互斥锁(函数级,写侧整段持锁、读侧每次持锁,无嵌套; + * 内部清空/解析用 nolock 私有版避免自锁死)。与 ncm lyric.c 的机制完全相同。 + * + * 内存纪律:静态存储(行数组 + 文本缓冲)零 malloc;仅 meow_lyric_parse 排序时在**堆**上 + * 申请瞬态索引/拷贝数组(ncm 实测:600 行×192 字节的临时数组若放栈会占 ~227KB,超出后台 + * 线程 128KB 栈 → 栈溢出 SIGSEGV;放堆按 s_n 计、用后即 free)。任一 malloc 失败保留未排序 + * (不崩,查询仍可用)。 + */ +#include "meow_lyric.h" + +#include +#include +#include +#include + +/* ---- 运行时状态(静态,解析/查询全程零 malloc;仅排序时瞬态堆)---- */ +static int64_t s_t[MEOW_LYRIC_LINES_MAX]; /* 每行时间戳(ms) */ +static char s_txt[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 原文 */ +static char s_tl[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 翻译 */ +static int s_n = 0; /* 有效行数 */ +static int g_max_lines = MEOW_LYRIC_LINES_MAX; /* 运行时可配行上限(默认 600) */ +static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER; + +/* 清空(nolock,内部用;调用方须已持锁)。 */ +static void meow_lyric_clear_nolock(void) +{ + s_n = 0; + memset(s_t, 0, sizeof(s_t)); + memset(s_txt, 0, sizeof(s_txt)); + memset(s_tl, 0, sizeof(s_tl)); +} + +/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行, + * 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。 + * 语义与 ncm copy_lrc_text 一致。 */ +static void meow_lyric_copy_text(char *dst, const char *src, size_t n, size_t max) +{ + size_t oi = 0, si = 0; + while (si < n && oi < max - 1) { + if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) { + dst[oi++] = ' '; + si += 2; + /* \r\n / \n\r 成对 → 只留一个空格(跳过紧随的互补转义,防双空格) */ + if (si + 1 < n && src[si] == '\\' && + ((src[si + 1] == 'r' && src[si - 1] == 'n') || + (src[si + 1] == 'n' && src[si - 1] == 'r'))) + si += 2; + } else { + dst[oi++] = src[si++]; + } + } + dst[oi] = 0; +} + +/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后); + * 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。 + * 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1, + * 网易云 LRC 毫秒)、>3 位截断到毫秒。 */ +static int meow_lyric_parse_tag(const char *p, int64_t *out_ms, const char **next) +{ + const char *q = p + 1; /* 跳过 '[' */ + int m = 0, md = 0, s = 0, sd = 0; + while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; } + if (md == 0 || *q != ':') goto notime; + q++; + while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; } + if (sd == 0) goto notime; + long frac = 0; int fd = 0; long scale = 10; + if (*q == '.') { + q++; + while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; } + if (fd == 1) scale = 100; + else if (fd == 2) scale = 10; + else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */ + else scale = 0; /* 孤立 '.' 无数字:忽略小数 */ + } + if (*q != ']') goto notime; + *out_ms = (int64_t)m * 60000L + (int64_t)s * 1000L + frac * scale; + *next = q + 1; + return 1; +notime: + { + const char *r = strchr(p, ']'); + *next = r ? r + 1 : NULL; + return 0; + } +} + +/* 解析一批 LRC 行(原文 is_trans=0 / 翻译 is_trans=1)。翻译按 first_ms 匹配到已有原文行合并。 + * 行上限取 g_max_lines;超出的原文行丢弃。不持锁(调用方已持锁)。 */ +static void meow_lyric_parse_lines(const char *lrc, int is_trans) +{ + if (!lrc) return; + const char *p = lrc; + while (*p) { + while (*p && *p != '[') p++; + if (!*p) break; + /* 解析一个或多个时间标签 */ + int have = 0; + int64_t first_ms = 0; + while (*p == '[') { + int64_t ms; + const char *after; + if (meow_lyric_parse_tag(p, &ms, &after)) { + if (!have) { first_ms = ms; have = 1; } + p = after; + } else { + /* 非时间标签([ti:..] 等)——跳到行尾 */ + if (!after) return; /* 无闭合 ']':残缺文件 */ + p = after; + break; + } + } + if (have) { + /* 正文 = 到行尾(去 \r\n) */ + const char *e = p; + while (*e && *e != '\n' && *e != '\r') e++; + size_t len = (size_t)(e - p); + if (len > MEOW_LYRIC_TEXT_MAX - 1) len = MEOW_LYRIC_TEXT_MAX - 1; + if (is_trans) { + /* 翻译行:按 first_ms 匹配到已有原文行 */ + for (int i = 0; i < s_n; i++) { + if (s_t[i] == first_ms) { + meow_lyric_copy_text(s_tl[i], p, len, MEOW_LYRIC_TEXT_MAX); + break; + } + } + } else { + if (s_n < g_max_lines) { + s_t[s_n] = first_ms; + meow_lyric_copy_text(s_txt[s_n], p, len, MEOW_LYRIC_TEXT_MAX); + s_tl[s_n][0] = 0; + s_n++; + } + } + } + while (*p && *p != '\n') p++; /* 跳到下一行 */ + if (*p == '\n') p++; + } +} + +/* qsort 比较:按时间戳升序(用索引数组。s_t 为静态数组,比较器直接读它)。 */ +static int meow_lyric_cmp(const void *a, const void *b) +{ + const int *ia = (const int *)a, *ib = (const int *)b; + if (s_t[*ia] < s_t[*ib]) return -1; + if (s_t[*ia] > s_t[*ib]) return 1; + return 0; +} + +/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。 + * 保证 meow_lyric_wrap 在无渲染引擎下仍行为确定(maxw 作字节预算,仍按中西文断行)。 */ +static int meow_lyric_default_width(const char *u, int size, void *ctx) +{ + (void)size; (void)ctx; + return (int)strlen(u); +} + +void meow_lyric_clear(void) +{ + pthread_mutex_lock(&g_lyric_mu); + meow_lyric_clear_nolock(); + pthread_mutex_unlock(&g_lyric_mu); +} + +int meow_lyric_parse(const char *lrc, const char *tlrc) +{ + pthread_mutex_lock(&g_lyric_mu); + meow_lyric_clear_nolock(); + meow_lyric_parse_lines(lrc, 0); + meow_lyric_parse_lines(tlrc, 1); + /* 按时间排序(用索引数组)。临时数组放**堆**上:`char [600][192]`×2 + `int64[600]` + * 若放栈会占 ~227KB,超过后台线程 128KB 栈 → 栈溢出 SIGSEGV。堆上按 s_n 计 + * (≤600 行),峰值 ~230KB 瞬态、用后即 free。 */ + if (s_n > 0) { + int *idx = (int *)malloc(sizeof(int) * (size_t)s_n); + int64_t *ttmp = (int64_t *)malloc(sizeof(int64_t) * (size_t)s_n); + char (*txt)[MEOW_LYRIC_TEXT_MAX] = + (char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n); + char (*tl)[MEOW_LYRIC_TEXT_MAX] = + (char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n); + if (idx && ttmp && txt && tl) { + for (int i = 0; i < s_n; i++) idx[i] = i; + qsort(idx, (size_t)s_n, sizeof(int), meow_lyric_cmp); + memcpy(ttmp, s_t, sizeof(int64_t) * (size_t)s_n); + memcpy(txt, s_txt, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n); + memcpy(tl, s_tl, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n); + for (int i = 0; i < s_n; i++) { + s_t[i] = ttmp[idx[i]]; + memcpy(s_txt[i], txt[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX); + memcpy(s_tl[i], tl[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX); + } + } + free(idx); free(ttmp); free(txt); free(tl); /* 任一 malloc 失败:保留未排序(不崩) */ + } + int result = s_n; + pthread_mutex_unlock(&g_lyric_mu); + return result; +} + +int meow_lyric_count(void) +{ + pthread_mutex_lock(&g_lyric_mu); + int n = s_n; + pthread_mutex_unlock(&g_lyric_mu); + return n; +} + +int meow_lyric_line_at(int64_t ms) +{ + pthread_mutex_lock(&g_lyric_mu); + int line = 0; + for (int i = 0; i < s_n; i++) { + if (s_t[i] <= ms) line = i; + else break; + } + pthread_mutex_unlock(&g_lyric_mu); + return line; +} + +const char *meow_lyric_line_text(int i) +{ + pthread_mutex_lock(&g_lyric_mu); + const char *p = (i < 0 || i >= s_n) ? "" : s_txt[i]; + pthread_mutex_unlock(&g_lyric_mu); + return p; +} + +const char *meow_lyric_line_tl(int i) +{ + pthread_mutex_lock(&g_lyric_mu); + const char *p = (i < 0 || i >= s_n) ? "" : s_tl[i]; + pthread_mutex_unlock(&g_lyric_mu); + return p; +} + +int64_t meow_lyric_line_time(int i) +{ + pthread_mutex_lock(&g_lyric_mu); + int64_t t = (i < 0 || i >= s_n) ? -1 : s_t[i]; + pthread_mutex_unlock(&g_lyric_mu); + return t; +} + +void meow_lyric_set_max_lines(int max) +{ + pthread_mutex_lock(&g_lyric_mu); + if (max < 1) max = 1; + if (max > MEOW_LYRIC_LINES_MAX) max = MEOW_LYRIC_LINES_MAX; + g_max_lines = max; + pthread_mutex_unlock(&g_lyric_mu); +} + +int meow_lyric_wrap(const char *txt, int size, int maxw, + char seg[][MEOW_LYRIC_SEG_SZ], int max_rows, + meow_lyric_width_fn width, void *ctx) +{ + meow_lyric_width_fn w = width ? width : meow_lyric_default_width; + if (!txt || !txt[0] || !seg || max_rows <= 0) { + if (seg) seg[0][0] = 0; + return 1; + } + int n = 0; + size_t len = strlen(txt), pos = 0; + while (pos < len && n < max_rows) { + size_t start = pos; + int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */ + int overflow = 0; + while (pos < len) { + unsigned char c = (unsigned char)txt[pos]; + int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4; + if (pos + (size_t)ch > len) ch = (int)(len - pos); + size_t cand = (pos + (size_t)ch) - start; + size_t cp = cand < MEOW_LYRIC_SEG_SZ - 1 ? cand : MEOW_LYRIC_SEG_SZ - 1; + memcpy(seg[n], txt + start, cp); seg[n][cp] = 0; + if (w(seg[n], size, ctx) > maxw && cand > 0) { overflow = 1; break; } + if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start); + pos += (size_t)ch; + } + if (overflow) { + size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start); + if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */ + unsigned char c = (unsigned char)txt[start]; + int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4; + linelen = (size_t)ch; + } + size_t cp = linelen < MEOW_LYRIC_SEG_SZ - 1 ? linelen : MEOW_LYRIC_SEG_SZ - 1; + memcpy(seg[n], txt + start, cp); seg[n][cp] = 0; + pos = start + linelen; + if (last_ws > 0) { + size_t sp = start + (size_t)last_ws + 1; + if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */ + } + n++; + } else { + size_t cp = (len - start) < MEOW_LYRIC_SEG_SZ - 1 ? (len - start) : MEOW_LYRIC_SEG_SZ - 1; + memcpy(seg[n], txt + start, cp); seg[n][cp] = 0; + n++; + break; + } + } + if (n == 0) { seg[0][0] = 0; n = 1; } + return n; +} diff --git a/patch_v2/sdk/meow_lyric.h b/patch_v2/sdk/meow_lyric.h new file mode 100644 index 0000000..135a313 --- /dev/null +++ b/patch_v2/sdk/meow_lyric.h @@ -0,0 +1,82 @@ +/* + * meow_lyric.h — MeowOS SDK 通用歌词组件(LRC 时间轴解析 / 存取 / 查询 + 折行辅助)。 + * + * 定位:把网易云 ncm_app 的歌词解析/查询能力(app/logic/lyric.c 的 parse/clear/count/ + * line_at/line_text/line_tl + copy_lrc_text 的「字面 \n 当换行」语义 + player.c 的 + * lyr_wrap_lines 折行)**通用化上收到 SDK**,供 launcher / ncm 等任意 App 复用。 + * 本头为**独立公共头**(仿 meow_ui_view.h 模式:不随 meow_ui.h 伞头发布,App 按需 + * `#include "meow_lyric.h"`),命名与 SDK 规范一致:API 一律 `meow_lyric_*`。 + * + * 输入:标准 LRC 行 `[mm:ss.xx]正文`。lrc=原文,tlrc=翻译。解析把带时间戳的行按时间升序 + * 排序存 timestamp(ms)/text;翻译按相同 timestamp 合并到对应原文行(存 tl),无匹配的翻译行 + * 忽略。流式、有界:行数上限默认 MEOW_LYRIC_LINES_MAX(600),运行时可经 + * meow_lyric_set_max_lines 下调;单行文本上限 MEOW_LYRIC_TEXT_MAX(192)字节。 + * + * 服务器字面序列:网易云服务器把多行文本用字面 `\n`/`\r` 塞进一行(渲染成豆腐块/凌乱), + * 解析时按空格替换(`\r\n`/`\n\r` 折成单空格),语义与 ncm copy_lrc_text 一致。 + * + * 线程安全(单写多读,机制与 ncm lyric.c 相同的交错安全):数据由后台线程写 + * (meow_lyric_parse / meow_lyric_clear / meow_lyric_set_max_lines),UI 线程读 + * (meow_lyric_count / line_at / line_text / line_tl / line_time)。全部函数内部持同一把 + * pthread 互斥锁,函数级粒度,写侧整段持锁、读侧每次访问持锁,无嵌套(清空/解析用 nolock + * 私有版,避免二次取锁自锁死)。**读接口返回的指针指向静态存储,在下次 meow_lyric_parse / + * meow_lyric_clear 之前有效**;调用方不得跨重建持有。这是与 ncm 完全相同的「指针在下次写前 + * 有效」契约。 + * + * 依赖:纯 libc(stdlib/string/stdio)+ pthread,不依赖其它 SDK 对象(不含 meow_log,便于 + * launcher 等非 meow_ui.a 使用方独立链入)。宿主 gcc 可直接编译单测。 + * + * 时间戳小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1, + * 网易云 LRC 毫秒)、>3 位截断到毫秒 —— 避免把网易云 3 位毫秒当 2 位厘秒解析导致时间放大。 + */ +#ifndef MEOW_LYRIC_H +#define MEOW_LYRIC_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- 容量 / 静态存储 ---- */ +#define MEOW_LYRIC_LINES_MAX 600 /* 静态行数组硬上限(对齐 ncm NCM_LYRIC_LINES) */ +#define MEOW_LYRIC_TEXT_MAX 192 /* 单行文本(原文/翻译)字节上限(对齐 ncm LRC_MAX_TEXT) */ +#define MEOW_LYRIC_SEG_SZ 256 /* 折行单段缓冲字节上限(对齐 ncm LYR_SEG_SZ) */ + +/* ---- 折行宽测量回调 ---- + * 由使用方注入(歌词视图传 meow_ui_text_width 的包装;宿主测传自己的测量函数)。 + * 返回 utf8 段在字号 size 下的宽度(px)。NULL 时回退为「字节数近似」(非像素精确,仅兜底)。 */ +typedef int (*meow_lyric_width_fn)(const char *utf8, int size, void *ctx); + +/* ---- 解析 / 清空 ---- + * meow_lyric_parse:重置并解析 lrc(原文)+ tlrc(翻译),按时间升序排序后返回歌词行数 + * (=meow_lyric_count 的值;<- 解析失败/空输入返回 0)。翻译按相同时间戳合并到对应原文行。 + * meow_lyric_clear:清空全部数据(下次 read 返回空)。 */ +void meow_lyric_clear(void); +int meow_lyric_parse(const char *lrc, const char *tlrc); + +/* ---- 查询(全部线程安全;line_text/tl 返回静态串,越界返回 "")---- */ +int meow_lyric_count(void); +int meow_lyric_line_at(int64_t ms); /* 当前行下标:last timestamp<=ms;首戳前=0;末戳后=n-1 */ +const char *meow_lyric_line_text(int i); /* 第 i 行原文;越界 "" */ +const char *meow_lyric_line_tl(int i); /* 第 i 行翻译;越界/无翻译 "" */ +int64_t meow_lyric_line_time(int i); /* 第 i 行时间戳(ms);越界 -1(供「点行跳转」seek) */ + +/* ---- 行上限配置 ---- + * 缺省 MEOW_LYRIC_LINES_MAX(600)。可下调至 [1, MEOW_LYRIC_LINES_MAX];下一次 parse 起生效 + * (解析行数超过上限时超出的行被截断丢弃)。 */ +void meow_lyric_set_max_lines(int max); + +/* ---- 折行辅助(上收 ncm lyr_wrap_lines,非线程相关、纯函数)---- + * 把 txt 按「词/字符」折成 ≤max_rows 段,每段宽度 ≤maxw px(宽测由 width 回调给出;中文按字符、 + * 英文按词断行)。段写入调用方 seg[][MEOW_LYRIC_SEG_SZ](seg 须能容纳 max_rows 个段), + * 返回段数(≥1;txt 空/超短返回 1 段空串)。单字符超宽时强制容纳该字符,防止死循环。 */ +int meow_lyric_wrap(const char *txt, int size, int maxw, + char seg[][MEOW_LYRIC_SEG_SZ], int max_rows, + meow_lyric_width_fn width, void *ctx); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_LYRIC_H */ diff --git a/patch_v2/sdk/test_meow_lyric b/patch_v2/sdk/test_meow_lyric new file mode 100755 index 0000000000000000000000000000000000000000..ff0bf26282f25c74479e9b8593412e42cb4eb495 GIT binary patch literal 72448 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

?b|f_ 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 literal 0 HcmV?d00001 diff --git a/patch_v2/sdk/test_meow_lyric.c b/patch_v2/sdk/test_meow_lyric.c new file mode 100644 index 0000000..0f44630 --- /dev/null +++ b/patch_v2/sdk/test_meow_lyric.c @@ -0,0 +1,205 @@ +/* + * test_meow_lyric.c — meow_lyric_* 宿主单测(2026-09-03,SDK 通用歌词组件批1)。 + * + * 编译(宿主,meow_lyric 为纯 libc + pthread,无需 SDK 其它对象): + * gcc -Wall -Wextra -pthread -o test_meow_lyric test_meow_lyric.c meow_lyric.c -I. + * ./test_meow_lyric + * + * 覆盖矩阵: + * T1 解析基础 + 乱序自动按时间升序排序(原文行) + * T2 meow_lyric_line_at 边界:首戳前 / 首戳 / 区间 / 恰逢戳 / 末戳后 / 负值 + * T3 翻译合并:按相同 timestamp 匹配合并;无匹配翻译行忽略 + * T4 服务器字面 \n / \r 当换行(含 \r\n 折叠成单空格) + * T5 行上限截断:meow_lyric_set_max_lines 下调后 parse 超限行丢弃 + * T6 时间戳小数位自适应:1 位(0.1s) / 2 位(厘秒) / 3 位(网易云毫秒) / >3 位截断到 ms + * T7 越界访问:line_text/line_tl 越界返回 "";line_time 越界返回 -1 + * T8 meow_lyric_clear:清空后 count=0 / text="" / line_time=-1;空输入 parse 返回 0 + * T9 折行 meow_lyric_wrap:中西文混排断行(英文按词、中文按字符)+ max_rows 截断 + * + 单字符超宽防死循环 + width 回调为 NULL 时按字节近似兜底 + */ +#include "meow_lyric.h" + +#include +#include + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (cond) { printf(" PASS: %s\n", msg); } \ + else { printf(" FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail = 1; } \ +} while (0) + +/* 折行宽测量:ASCII=1 单位,多字节(CJK 等)=2 单位(用于中西文断行测试)。 */ +static int uw(const char *u, int size, void *ctx) +{ + (void)size; (void)ctx; + int w = 0; + for (const char *p = u; *p; ) { + unsigned char c = (unsigned char)*p; + int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4; + w += (c < 0x80) ? 1 : 2; + p += ch; + } + return w; +} + +static void t1_basic(void) +{ + printf("T1 解析基础 + 乱序按时间升序排序\n"); + /* 输入乱序;期望按时间戳升序存(Line B@1000 < Line C@2000 < Line A@3000) */ + const char *lrc = "[00:03.00]Line A\n[00:01.00]Line B\n[00:02.00]Line C\n"; + int n = meow_lyric_parse(lrc, NULL); + CHECK(n == 3, "count==3"); + CHECK(meow_lyric_count() == 3, "count()==3"); + CHECK(strcmp(meow_lyric_line_text(0), "Line B") == 0, "idx0==Line B (1000ms 升序最前)"); + CHECK(strcmp(meow_lyric_line_text(1), "Line C") == 0, "idx1==Line C"); + CHECK(strcmp(meow_lyric_line_text(2), "Line A") == 0, "idx2==Line A (3000ms 最后)"); + CHECK(meow_lyric_line_time(0) == 1000, "line_time(0)==1000"); + CHECK(meow_lyric_line_time(2) == 3000, "line_time(2)==3000"); +} + +static void t2_line_at(void) +{ + printf("T2 meow_lyric_line_at 边界\n"); + const char *lrc = "[00:01.00]a\n[00:02.00]b\n[00:03.00]c\n"; + meow_lyric_parse(lrc, NULL); + CHECK(meow_lyric_line_at(500) == 0, "首戳前(500ms)→0"); + CHECK(meow_lyric_line_at(1000) == 0, "恰逢首戳→0"); + CHECK(meow_lyric_line_at(1500) == 0, "首区间(1500ms)→0"); + CHECK(meow_lyric_line_at(2500) == 1, "第二区间(2500ms)→1"); + CHECK(meow_lyric_line_at(3000) == 2, "恰逢末戳→2"); + CHECK(meow_lyric_line_at(99999) == 2, "末戳后→n-1"); + CHECK(meow_lyric_line_at(-100) == 0, "负值→0"); +} + +static void t3_translation(void) +{ + printf("T3 翻译合并(同 timestamp;无匹配翻译行忽略)\n"); + const char *lrc = "[00:02.00]Hello\n[00:05.00]World\n"; + const char *tlrc = "[00:05.00]你好\n[00:02.00]你好世界\n[01:00.00]no match line\n"; + int n = meow_lyric_parse(lrc, tlrc); + CHECK(n == 2, "count==2(原文行数)"); + CHECK(strcmp(meow_lyric_line_text(0), "Hello") == 0, "idx0原文 Hello"); + CHECK(strcmp(meow_lyric_line_tl(0), "你好世界") == 0, "idx0翻译 你好世界(按 2000ms 合并)"); + CHECK(strcmp(meow_lyric_line_text(1), "World") == 0, "idx1原文 World"); + CHECK(strcmp(meow_lyric_line_tl(1), "你好") == 0, "idx1翻译 你好(按 5000ms 合并)"); + /* 01:00.00 无对应原文行 → 丢弃,不新增行也不污染其它翻译 */ + CHECK(meow_lyric_count() == 2, "无匹配翻译行被丢弃(仍 2 行)"); +} + +static void t4_literal_newline(void) +{ + printf("T4 服务器字面 \\n / \\r 当换行\n"); + /* C 源码里 \\n 是字面反斜杠+n(服务器塞进一行的多行文本) */ + const char *lrc = "[00:01.00]line1\\nline2\r\n[00:02.00]a\\r\\nb\n"; + meow_lyric_parse(lrc, NULL); + CHECK(strcmp(meow_lyric_line_text(0), "line1 line2") == 0, "字面 \\n → 空格"); + CHECK(strcmp(meow_lyric_line_text(1), "a b") == 0, "字面 \\r\\n 成对 → 单空格"); +} + +static void t5_limit(void) +{ + printf("T5 行上限截断(set_max_lines 下调后超限丢弃)\n"); + meow_lyric_set_max_lines(2); + const char *lrc = "[00:01]a\n[00:02]b\n[00:03]c\n[00:04]d\n"; + int n = meow_lyric_parse(lrc, NULL); + CHECK(n == 2, "count==2(上限 2)"); + CHECK(strcmp(meow_lyric_line_text(0), "a") == 0, "idx0==a"); + CHECK(strcmp(meow_lyric_line_text(1), "b") == 0, "idx1==b(第三个 c 被截断)"); + meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX); /* 恢复默认 */ + n = meow_lyric_parse(lrc, NULL); + CHECK(n == 4, "恢复上限后 count==4"); + /* set_max_lines 越界 clamp */ + meow_lyric_set_max_lines(0); + n = meow_lyric_parse(lrc, NULL); + CHECK(n == 1, "set_max_lines(0)→clamp=1,count==1"); + meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX); +} + +static void t6_timefrac(void) +{ + printf("T6 时间戳小数位自适应\n"); + const char *lrc = + "[00:01.5]a\n" /* 1 位 → 1500ms */ + "[00:02.25]b\n" /* 2 位 → 2250ms */ + "[00:03.456]c\n" /* 3 位(网易云毫秒) → 3456ms */ + "[00:04.1234]d\n"; /* >3 位截断到 ms → 4123ms */ + meow_lyric_parse(lrc, NULL); + CHECK(meow_lyric_line_time(0) == 1500, "1 位小数 → 1500ms"); + CHECK(meow_lyric_line_time(1) == 2250, "2 位小数 → 2250ms"); + CHECK(meow_lyric_line_time(2) == 3456, "3 位小数 → 3456ms"); + CHECK(meow_lyric_line_time(3) == 4123, ">3 位小数截断 → 4123ms"); + /* 元数据行 [ti:..] 应跳过,不计入行(与歌词须在独立行;ncm 语义:首个非时间标签即令该行失效) */ + meow_lyric_parse("[ti:Test]\n[ar:Someone]\n[00:01.00]x\n", NULL); + CHECK(meow_lyric_count() == 1, "元数据 [ti:..]/[ar:..] 行不被当作歌词"); +} + +static void t7_oob(void) +{ + printf("T7 越界访问\n"); + meow_lyric_parse("[00:01.00]x\n[00:02.00]y\n", NULL); + CHECK(strcmp(meow_lyric_line_text(-1), "") == 0, "line_text(-1)==\"\""); + CHECK(strcmp(meow_lyric_line_text(99), "") == 0, "line_text(99)==\"\""); + CHECK(strcmp(meow_lyric_line_tl(-1), "") == 0, "line_tl(-1)==\"\""); + CHECK(strcmp(meow_lyric_line_tl(99), "") == 0, "line_tl(99)==\"\""); + CHECK(meow_lyric_line_time(-1) == -1, "line_time(-1)==-1"); + CHECK(meow_lyric_line_time(99) == -1, "line_time(99)==-1"); +} + +static void t8_clear_and_empty(void) +{ + printf("T8 clear / 空输入\n"); + meow_lyric_parse("[00:01.00]x\n", NULL); + meow_lyric_clear(); + CHECK(meow_lyric_count() == 0, "clear 后 count==0"); + CHECK(strcmp(meow_lyric_line_text(0), "") == 0, "clear 后 line_text(0)==\"\""); + CHECK(meow_lyric_line_time(0) == -1, "clear 后 line_time(0)==-1"); + CHECK(meow_lyric_parse(NULL, NULL) == 0, "parse(NULL,NULL)→0"); + CHECK(meow_lyric_count() == 0, "空解析后 count==0"); +} + +static void t9_wrap(void) +{ + printf("T9 折行 meow_lyric_wrap\n"); + char seg[8][MEOW_LYRIC_SEG_SZ]; + + /* 中西文:英文按词断行、中文按字符断行 → "Hello" / "世界" 两段 */ + int r = meow_lyric_wrap("Hello 世界", 28, 6, seg, 8, uw, NULL); + CHECK(r == 2, "中西文折为 2 段"); + CHECK(strcmp(seg[0], "Hello") == 0, "seg0==Hello(词边界断)"); + CHECK(strcmp(seg[1], "世界") == 0, "seg1==世界(中文按字符)"); + + /* max_rows 截断:每单词一 bar,max_rows=3 → 只出 3 段 */ + r = meow_lyric_wrap("a b c d e f g", 28, 1, seg, 3, uw, NULL); + CHECK(r == 3, "max_rows=3 截断为 3 段"); + + /* 单字符超宽(maxw=0):能放一个字符且不死循环,返回完整逐字符段 */ + r = meow_lyric_wrap("abc", 28, 0, seg, 5, uw, NULL); + CHECK(r == 3, "maxw=0 逐字符折为 3 段(无死循环)"); + CHECK(strcmp(seg[0], "a") == 0 && strcmp(seg[2], "c") == 0, "seg0==a / seg2==c"); + + /* width 回调 NULL → 字节近似兜底(maxw 作字节预算) */ + r = meow_lyric_wrap("abcd", 28, 2, seg, 4, NULL, NULL); + CHECK(r == 2, "width=NULL 按字节近似折为 2 段"); + CHECK(strcmp(seg[0], "ab") == 0 && strcmp(seg[1], "cd") == 0, "seg0==ab / seg1==cd"); + + /* 空/超短:txt 空 → 1 段空串 */ + r = meow_lyric_wrap("", 28, 10, seg, 4, uw, NULL); + CHECK(r == 1 && seg[0][0] == 0, "空文本 → 1 段空串"); + CHECK(meow_lyric_wrap(NULL, 28, 10, seg, 4, uw, NULL) == 1, "NULL 文本 → 1 段"); +} + +int main(void) +{ + printf("== meow_lyric 宿主单测 ==\n"); + t1_basic(); + t2_line_at(); + t3_translation(); + t4_literal_newline(); + t5_limit(); + t6_timefrac(); + t7_oob(); + t8_clear_and_empty(); + t9_wrap(); + printf("%s\n", g_fail ? "== FAIL ==" : "== PASS =="); + return g_fail ? 1 : 0; +}