feat(sdk): 批1 meow_lyric 通用歌词组件 — lrc/tlrc解析+存取+查询+折行辅助,纯libc+pthread零依赖,宿主54断言全过,暂不并入meow_ui.a

This commit is contained in:
AskaEth committed 2026-09-03 14:40:44 +08:00
1 parent b5da7d7151
commit 42718385cc
4 files changed
+601

No files matched your search

+314
View File
@@ -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;
}