- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
238 lines
8.8 KiB
C
238 lines
8.8 KiB
C
/* logic/lyric.c — LRC 时间轴解析 + 当前行定位。
|
||
*
|
||
* 输入:ncm_api_lyric 返回的 lrc(原文)与 tlrc(翻译)。两者均为标准 [mm:ss.xx] 行。
|
||
* 解析:把所有带时间戳的行按时间升序排序,存 timestamp(ms)/text;翻译按相同时间戳合并到
|
||
* 对应原文行(存 tl)。无匹配的翻译行忽略。流式、有界:NCM_LYRIC_LINES 行,超限截断。
|
||
* 纯 C 无 UI,仅依赖标准库。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
/* 歌词数据由 lyric_worker(player_manager.c,后台线程)写(lyric_parse/lyric_clear)而 UI 线程
|
||
* 与 IPC 线程读(lyric_count/lyric_line_at/lyric_line_text/lyric_line_tl)。三期第一批(REPORT §14.3)
|
||
* 补一把锁消除**数据竞争**(早前为良性竞态 §10.9 FM4;本节要求禁止数据竞争)。锁粒度=函数级,
|
||
* 写侧在 lyric_parse 整段持锁、读侧每次访问持锁,无嵌套(清空用 nolock 版,避免二次取锁自锁死)。 */
|
||
#define LRC_MAX_TEXT 192
|
||
static long s_t[NCM_LYRIC_LINES];
|
||
static char s_txt[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||
static char s_tl[NCM_LYRIC_LINES][LRC_MAX_TEXT];
|
||
static int s_n;
|
||
static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER;
|
||
|
||
static void 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。 */
|
||
static void copy_lrc_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 毫秒)——此前固定 ×10 把网易云 3 位毫秒当 2 位厘秒解析,时间戳放大 ~10 倍,
|
||
* 歌词行按绝对时间激活滞后 ~4~10s(REPORT「英文歌歌词滚动慢」)。 */
|
||
static int parse_lrc_tag(const char *p, long *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 = (long)m * 60000L + (long)s * 1000L + frac * scale;
|
||
*next = q + 1;
|
||
return 1;
|
||
notime:
|
||
{
|
||
const char *r = strchr(p, ']');
|
||
*next = r ? r + 1 : NULL;
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static void lrc_parse(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;
|
||
long first_ms = 0;
|
||
while (*p == '[') {
|
||
long ms;
|
||
const char *after;
|
||
if (parse_lrc_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 > LRC_MAX_TEXT - 1) len = LRC_MAX_TEXT - 1;
|
||
if (is_trans) {
|
||
/* 翻译行:按 first_ms 匹配到已有原文行 */
|
||
for (int i = 0; i < s_n; i++) {
|
||
if (s_t[i] == first_ms) {
|
||
copy_lrc_text(s_tl[i], p, len, LRC_MAX_TEXT);
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
if (s_n < NCM_LYRIC_LINES) {
|
||
s_t[s_n] = first_ms;
|
||
copy_lrc_text(s_txt[s_n], p, len, LRC_MAX_TEXT);
|
||
s_tl[s_n][0] = 0;
|
||
s_n++;
|
||
}
|
||
}
|
||
}
|
||
while (*p && *p != '\n') p++; /* 跳到下一行 */
|
||
if (*p == '\n') p++;
|
||
}
|
||
}
|
||
|
||
static int cmp_lines(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;
|
||
}
|
||
|
||
int lyric_parse(const char *lrc, const char *tlrc)
|
||
{
|
||
pthread_mutex_lock(&g_lyric_mu);
|
||
lyric_clear_nolock();
|
||
int result = 0;
|
||
lrc_parse(lrc, 0);
|
||
lrc_parse(tlrc, 1);
|
||
/* 按时间排序(用索引数组)。临时数组放**堆**上:`char [600][192]`×2 + `long[600]`
|
||
* 若放栈会占 ~227KB,超过任何线程的 128KB 栈(ncm_thread_create NCM_THREAD_STACK)
|
||
* → lyric_worker 起播解析时栈溢出 SIGSEGV(真机 sig=11,RA 落在 lyric_parse)。
|
||
* 堆上按 s_n 计(≤600 行),峰值 ~230KB 瞬态、用后即 free。 */
|
||
if (s_n > 0) {
|
||
int *idx = (int *)malloc(sizeof(int) * (size_t)s_n);
|
||
long *ttmp = (long *)malloc(sizeof(long) * (size_t)s_n);
|
||
char (*txt)[LRC_MAX_TEXT] =
|
||
(char (*)[LRC_MAX_TEXT])malloc((size_t)LRC_MAX_TEXT * (size_t)s_n);
|
||
char (*tl)[LRC_MAX_TEXT] =
|
||
(char (*)[LRC_MAX_TEXT])malloc((size_t)LRC_MAX_TEXT * (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), cmp_lines);
|
||
memcpy(ttmp, s_t, sizeof(long) * (size_t)s_n);
|
||
memcpy(txt, s_txt, (size_t)LRC_MAX_TEXT * (size_t)s_n);
|
||
memcpy(tl, s_tl, (size_t)LRC_MAX_TEXT * (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]], LRC_MAX_TEXT);
|
||
memcpy(s_tl[i], tl[idx[i]], LRC_MAX_TEXT);
|
||
}
|
||
}
|
||
free(idx); free(ttmp); free(txt); free(tl); /* 任一 malloc 失败:保留未排序(不崩) */
|
||
}
|
||
result = s_n;
|
||
pthread_mutex_unlock(&g_lyric_mu);
|
||
return result;
|
||
}
|
||
|
||
int 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 *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 *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;
|
||
}
|
||
|
||
/* 第 i 行时间戳(ms)。越界返回 -1。供播放页「点行跳转」(§22:seek 到该行时间)。 */
|
||
long lyric_line_time(int i)
|
||
{
|
||
pthread_mutex_lock(&g_lyric_mu);
|
||
long t = (i < 0 || i >= s_n) ? -1 : s_t[i];
|
||
pthread_mutex_unlock(&g_lyric_mu);
|
||
return t;
|
||
}
|
||
|
||
int lyric_count(void)
|
||
{
|
||
pthread_mutex_lock(&g_lyric_mu);
|
||
int n = s_n;
|
||
pthread_mutex_unlock(&g_lyric_mu);
|
||
return n;
|
||
}
|
||
|
||
void lyric_clear(void)
|
||
{
|
||
pthread_mutex_lock(&g_lyric_mu);
|
||
lyric_clear_nolock();
|
||
pthread_mutex_unlock(&g_lyric_mu);
|
||
}
|