feat(sdk): 批1 meow_lyric 通用歌词组件 — lrc/tlrc解析+存取+查询+折行辅助,纯libc+pthread零依赖,宿主54断言全过,暂不并入meow_ui.a
This commit is contained in:
1 parent
b5da7d7151
commit
42718385cc
4 files changed
+601
No files matched your search
@@ -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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
/* ---- 运行时状态(静态,解析/查询全程零 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;
|
||||||
|
}
|
||||||
@@ -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 <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#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 */
|
||||||
Executable
BIN
Binary file not shown.
@@ -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 <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user