音频调试与歌词
本文档介绍 AC792 SDK 中「音频调试」与「歌词(LRC)」两大能力的完整实现:歌词解析引擎(sdk/audio/common/lyrics)、公开数据结构(sdk/include_lib/utils/lyrics/lyrics.h)、网络歌词与本地歌词的接入方式、LVGL 歌词控件与 App 端 UI 集成,以及基于 audio_debug 宏和 uartPcmSender 的音频调试基础设施。
Purpose and Scope
本页覆盖以下内容:
- 歌词引擎:LRC 文件解析(GBK / UTF-16 / UTF-8 编码识别)、时间标签(TIME_LABEL)提取与排序、按播放时间定位歌词(
lrc_get/lrc_show)、翻页滚动速度控制、歌词时间标签保存到 Flash 的长歌词兼容方案。 - 歌词接入层:
lyrics_api.c提供的面向业务的 API(lyric_init/lrc_analysis_api/lrc_show_api/net_lrc_analysis_api等),以及文件 IO 抽象(LRC_FILE_IO),支持本地文件(fread/fseek)与网络下载(net_download_read/seek)两种数据源。 - 歌词 UI:LVGL v8 的
jl_extra歌词控件(lv_lyrics.h)、官方示例lv_example_lyrics_1~5,以及wifi_soundbox/wifi_camera工程中的实际集成代码(ui_action_music_lyrics.c、lyrics_anim_effect.c、ui_action_page_lyrics.c)。 - 音频调试:
debug/audio_debug.h头文件、audio_debug打印宏的使用范式,以及 PCM 数据经 UART 输出的调试通路(uartPcmSender.h)。
以下内容属于其他页面,不在本页展开:通用音频服务(播放/录音/解码器)、网络下载模块(net_download)本身的实现、LVGL 基础框架、具体产品 UI 风格(style_JL / style_LY)的完整页面逻辑。
Overview
歌词子系统在整个系统中的位置
在音乐播放类产品(wifi_soundbox、wifi_camera 等)中,歌词是播放体验的核心组成部分。歌词链路横跨四层:
- 数据源层:本地歌词文件(与音乐文件同目录的
.lrc)或网络下载的歌词数据流。 - 引擎层:
sdk/audio/common/lyrics/lyrics_api.c基于sdk/include_lib/utils/lyrics/lyrics.h中定义的核心结构,完成歌词文件的编码识别、时间标签解析、按时间点取词。 - 适配层:
LRC_FILE_IO函数指针表把文件读写抽象出来,本地文件走标准库fread/fseek,网络歌词走net_download_read/net_download_seek,引擎代码不感知数据来源。 - 显示层:引擎通过
lrc_show_api把当前时刻的歌词内容交给上层,上层再通过 LVGL 歌词控件(支持滚动动画、强调色等效果)渲染到屏幕。
设计意图:引擎层与平台/显示完全解耦。LRC_INFO 只维护"当前文件位置、已解析标签、当前应显示的歌词内容"等状态,不直接操作 LCD 或 LVGL;显示刷新由回调(roll_speed_ctrl_cb、clr_lrc_disp_cb)和上层轮询完成。这样同一套引擎可以服务本地播放、网络播放、桌面工具等多种场景。
音频调试的定位
嵌入式音频开发中,最常见的调试手段是打点日志与原始 PCM 数据抓取。SDK 提供:
audio_debug打印宏(定义于debug/audio_debug.h),用于输出音频链路的调试信息;uartPcmSender,通过 UART 将 PCM 数据发送到上位机(如杰理提供的音频分析工具),便于在 PC 端观察波形与频谱。
audio_general.c 同时包含这两个头文件,是音频公共模块的标准调试入口。
Architecture
下图展示歌词子系统与音频调试模块在 SDK 中的分层结构与数据流(节点名均为真实代码中的类型/文件):
flowchart TD
subgraph sg_App["App / UI 层"]
UIAction["ui_action_music_lyrics.c / ui_action_page_lyrics.c"]
AnimEffect["lyrics_anim_effect.c"]
LvglWidget["lv_lyrics.h (jl_extra 歌词控件)"]
LvglDemo["lv_example_lyrics_1~5.c"]
end
subgraph sg_Engine["歌词引擎层 (sdk/audio/common/lyrics)"]
LyricsApi["lyrics_api.c / lyrics_api.h"]
CoreEngine["lyrics.h: LRC_INFO / LRC_CFG / TIME_LABEL"]
MutexList["全局链表 head + OS_MUTEX mutex"]
end
subgraph sg_IO["数据源 / IO 适配层"]
LocalIO["LRC_FILE_IO: fread + fseek"]
NetIO["LRC_FILE_IO: net_download_read + net_download_seek"]
FlashSave["TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH 时间标签落 Flash"]
end
subgraph sg_Debug["音频调试"]
AudioDebugH["debug/audio_debug.h (audio_debug 宏)"]
UartPcm["uartPcmSender.h (PCM 经 UART 输出)"]
AudioGeneral["audio_general.c"]
AudioInput["audio_input.c (LLM 工程调用示例)"]
end
UIAction --> LyricsApi
UIAction --> LvglWidget
LvglWidget --> AnimEffect
LvglDemo --> LvglWidget
LyricsApi --> CoreEngine
LyricsApi --> MutexList
CoreEngine --> LocalIO
CoreEngine --> NetIO
CoreEngine --> FlashSave
LyricsApi --> LocalIO
LyricsApi --> NetIO
AudioGeneral --> AudioDebugH
AudioGeneral --> UartPcm
AudioInput --> AudioDebugH
架构说明
- 歌词引擎:
lyrics_api.c是业务入口,内部创建LRC_INFO(核心状态机),并把实例挂到全局链表head上(list_add_tail),通过os_mutex保护链表的并发访问;多实例(多播放器)场景下每个实例独立解析,互不干扰。 - IO 抽象:
LRC_FILE_IO是一对函数指针(seek/read)。本地文件用标准库函数,网络歌词用net_download接口,二者对引擎透明——这是net_lrc_analysis_api与lrc_analysis_api共用一个解析内核的关键。 - 音频调试:
audio_debug宏在audio_input.c中被大量使用(部分处于注释状态,便于按需开启);uartPcmSender与audio_debug.h一起被audio_general.c引入,构成音频链路调试的两条通路(日志 + 波形)。
歌词数据结构与状态机
编码识别与时间标签
lyrics.h 定义了四种歌词编码:
enum {
LRC_GBK = 0, //内码
LRC_UTF16_S, //unicode小端码
LRC_UTF16_B, //unicode大端码
LRC_UTF8,
};
Source: lyrics.h
时间标签是歌词的最小单元,TIME_LABEL 记录"第几秒、几百毫秒、歌词内容长度、下一条标签的文件字节偏移",其中 wline_pos 是解析器为了顺序读取而维护的"游标":
typedef struct _TIME_LABEL { /*时间标签信息[mm:ss.ms]*/
u16 dbtime_s; //time of label,unit:s
u8 btime_100ms; //time of label,unit:ms
u8 btext_len; //the length of lrc content, 占用的实际字节数
u32 wline_pos; //for record next (byte addr) after the (real label of time)
} TIME_LABEL;
Source: lyrics.h
LRC_INFO 与 LRC_CFG
LRC_INFO 是解析引擎的"运行时上下文",字段按职责分组:显示相关(bis_lrc_update、blcd_roll_speed、content_len)、文件相关(cur_faddr、once_read_len、real_len、label_id)、标签相关(bfirst_lable、blast_lable)、解析辅助(sorting、lab_info)、回调(roll_speed_ctrl_cb、clr_lrc_disp_cb)。LRC_CFG 则是初始化参数,二者分离的设计让同一套引擎代码可以按不同产品配置不同缓冲区与行为(如 read_next_lrc_flag 决定是否预读下一条歌词)。
引擎实现走读
模块初始化:lyric_init
lyric_init 是整个歌词模块的入口。它完成三件事:配置 LRC_CFG、按对齐规则一次性分配全部内存、把实例注册进全局链表。关键点在于内存布局——所有内部结构(LRC_INFO、LABEL_INFO、SORTING_INFO、显示缓存、标签缓存、读取缓存)都从同一块 zalloc 内存中按 4 字节对齐切分,避免多次分配带来的碎片化:
void *lyric_init(void)
{
LRC_CFG t_lrc_cfg = {0};
t_lrc_cfg.once_read_len = ONCE_READ_LENGTH;
t_lrc_cfg.once_disp_len = ONCE_DIS_LENGTH;
t_lrc_cfg.label_temp_buf_len = LABEL_TEMP_BUF_LEN;
t_lrc_cfg.roll_speed_ctrl_cb = lrc_roll_speed_ctrl;
t_lrc_cfg.clr_lrc_disp_cb = NULL;
t_lrc_cfg.read_next_lrc_flag = 0;
u32 need_buf_size = LRC_SIZEOF_ALIN(sizeof(LRC_INFO), 4)
+ LRC_SIZEOF_ALIN(sizeof(LABEL_INFO), 4)
+ LRC_SIZEOF_ALIN(sizeof(SORTING_INFO), 4)
+ LRC_SIZEOF_ALIN(t_lrc_cfg.once_disp_len, 4)
+ LRC_SIZEOF_ALIN(t_lrc_cfg.label_temp_buf_len, 4)
+ LRC_SIZEOF_ALIN(t_lrc_cfg.once_read_len, 4);
LRC_INFO *lrc_info = zalloc(need_buf_size);
if (!lrc_info) {
return NULL;
}
if (0 != lrc_param_init(&t_lrc_cfg, lrc_info)) {
free(lrc_info);
return NULL;
}
os_mutex_pend(&mutex, 0);
list_add_tail(&lrc_info->entry, &head);
os_mutex_post(&mutex);
return lrc_info;
}
Source: lyrics_api.c
设计意图:把"配置、分配、注册"收敛到一个函数里,上层只需保存返回的 void * 句柄;lyric_exit 对称地负责从链表摘除并释放内存。zalloc 保证初始状态全零,bis_lrc_update 等标志位默认即为"无更新",避免脏状态。
文件 IO 抽象与网络歌词
本地歌词与网络歌词共用解析内核,差别只在 LRC_FILE_IO 函数表。本地走标准库:
static const LRC_FILE_IO lrc_file_io = {
.seek = fseek,
.read = fread,
};
static int net_lrc_file_read(void *buf, u32 size, u32 count, FILE *file)
{
return net_download_read((void *)file, buf, size);
}
static int net_lrc_file_seek(FILE *file, int offset, int orig)
{
return net_download_seek((void *)file, offset, orig);
}
static const LRC_FILE_IO net_lrc_file_io = {
.seek = net_lrc_file_seek,
.read = net_lrc_file_read,
};
Source: lyrics_api.c
这是典型的策略模式:LRC_FILE_IO 即策略接口,本地文件策略与网络流策略是两种实现。网络侧把 net_download 句柄伪装成 FILE * 传入,net_lrc_analysis_api(lrc, net_lrc_file) 与 lrc_analysis_api(lrc, file, &newFile) 内部走同一套 lrc_analysis 解析逻辑。因此"边下边解析"(网络歌词流式播放)成为可能——解析器只需 seek/read 两个原语。
翻页滚动速度控制
歌词较长时需要滚动显示,滚动速度不能是固定值,否则要么跟不上演唱、要么滚太快。lrc_roll_speed_ctrl 根据"当前行歌词长度"和"与下一条歌词的时间间隔"计算滚动一屏的速度(单位 500ms):
static void lrc_roll_speed_ctrl(u8 lrc_len, u32 time_gap, u8 *roll_speed)
{
///DVcTxt1_11 显示长度为32 bytes
if (lrc_len > (LRC_DISPLAY_TEXT_LEN * 2)) {
*roll_speed = ((time_gap + 2) / 3) << 1;
} else if (lrc_len > LRC_DISPLAY_TEXT_LEN) {
*roll_speed = time_gap;
} else {
*roll_speed = 250; ///never load new page
}
}
Source: lyrics_api.c
规则解读:超过两屏的歌词,滚动速度约为时间间隔的 2/3 再乘 2(即更快滚完);一屏到两屏之间按间隔原速滚动;不足一屏则给 250(即"不要翻页",停留等待)。该回调通过 LRC_CFG.roll_speed_ctrl_cb 注入,产品可按显示区宽度定制。
解析与显示主流程
引擎对外暴露四个核心函数:lrc_param_init(按配置初始化实例)、lrc_analysis(解析文件建立时间标签表)、lrc_get(按时间取当前歌词)、lrc_show(取歌词并标记显示更新)。lyrics_api.c 把它们包装成业务 API:
lrc_analysis_api(lrc, file, &newFile):解析本地文件,newFile返回重定位后的文件指针(歌词文件可能嵌在容器里,解析后需知道音乐数据从哪开始);net_lrc_analysis_api(lrc, net_lrc_file):解析网络流;lrc_show_api(lrc, text_id, dbtime_s, btime_100ms):按播放时刻(秒 + 百毫秒)取出应显示的歌词,写入LRC_INFO.blrc_buf并置bis_lrc_update;lrc_get_api:仅查询不触发显示更新,用于上层预读。
显示数据由 LRC_DISPLAY_TEXT_ID(文本控件 ID)与 LRC_DISPLAY_TEXT_LEN(32 字节显示区)约束,ONCE_READ_LENGTH(128 字节对齐)与 ONCE_DIS_LENGTH(64 字节对齐)决定读取/显示缓存粒度。
长歌词兼容:时间标签落 Flash
lyrics_api.h 头注释给出了完整的长歌词兼容方案:默认情况下时间标签缓存在 RAM 的 LABEL_TEMP_BUF_LEN(2048 字节)中,只能容纳约 1024 个标签;更长的歌词文件解析会失败。使能 TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH 后,标签可保存到 Flash(需在 isd_tools.cfg 中把 LRIF_LEN 配置为 0x10000 并置 LRIF_OPT=1),从而支持任意长度的歌词文件:
///<保存歌词时间标签到flash,可以解决较长歌词文件不兼容问题,要使能该功能,需要做以下几点:
//1、配置isd_tools.cfg文件(参考vm区间分配, LRIF_LEN 默认设置为64K)
// LRIF_ADDR=AUTO;
// LRIF_LEN=0x10000;
// LRIF_OPT=1;
//2、使能TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH
//3、配置时间标签临时缓存buf大小LABEL_TEMP_BUF_LEN(2048表示可以解释最长包含1024个时间标签的歌词文件)
//注意:如果flash空间不够,只能关闭TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH,但是不兼容长歌词文件
Source: lyrics_api.h
这是典型的"以空间换兼容性"取舍:Flash 预留 64K 区间(LRIF_LEN=0x10000)时,标签表不再受 RAM 限制;若 Flash 空间紧张,则关闭该宏回到 RAM 缓存模式,代价是不兼容超长歌词。
音频调试实现
audio_debug 是贯穿音频链路的调试日志宏(定义于 debug/audio_debug.h,audio_general.c 在公共音频模块中统一引入)。其使用范式是"printf 风格 + 按需开关"——多数打点默认注释,排查问题时放开:
audio_debug("ty_device_net_audio %d", flag);
if (flag) {
...
audio_debug("_device_net_audio_play %d", flag);
...
audio_debug("_device_net_audio_recorder %d", flag);
Source: audio_input.c
在 audio_general.c 中,audio_debug.h 与 uartPcmSender.h 同时被包含,说明音频公共模块同时具备"日志"与"PCM 波形输出"两条调试通路——后者通过 UART 把原始 PCM 数据发往 PC 端音频分析工具,用于排查失真、爆音、EQ 效果等主观问题。audio_input.c 中的注释打点(如 audio_debug("device_get_voice_data no data")、audio_debug("cbuf_write full"))展示了典型用法:在静音检测、环形缓冲写满等关键分支处留痕,方便定位数据流断点。
核心流程
本地歌词播放流程
一次完整的本地歌词显示,从播放器启动到屏幕刷新,经历以下步骤:
sequenceDiagram
participant App as 应用层 (ui_action_music_lyrics)
participant Api as lyrics_api.c
participant Eng as 引擎 (lyrics.h LRC_INFO)
participant IO as LRC_FILE_IO (fread/fseek)
participant Lcd as LVGL 歌词控件
App->>Api: lyric_init()
Api->>Eng: lrc_param_init(cfg, lrc) 分配/切分内存
Api-->>App: lrc 句柄
App->>Api: lrc_analysis_api(lrc, file, &newFile)
Api->>IO: seek/read 逐块读取 .lrc
IO-->>Api: 原始数据(GBK/UTF8/UTF16)
Api->>Eng: 编码识别 + 解析 [mm:ss.ms] 时间标签
Eng-->>Api: LABEL_INFO 标签表就绪
App->>Api: 循环: lrc_show_api(lrc, text_id, t_s, t_100ms)
Api->>Eng: lrc_show() 二分定位当前标签
Eng-->>Api: 当前行歌词内容 + 滚动速度
Api-->>App: 歌词文本/更新标志
App->>Lcd: 刷新显示/启动滚动动画
Lcd-->>App: 渲染完成
网络歌词流程与差异
网络歌词复用同一解析内核,差异仅在数据源与生命周期:net_lrc_analysis_api 传入 net_download 句柄,走 net_lrc_file_io(net_download_read/seek)。解析与下载是"拉模式"——引擎需要多少读多少,天然支持流式歌词,无需等待整个文件下载完成。退出时先 lyric_exit(lrc) 释放引擎资源,再由网络模块关闭下载句柄。
时间标签定位算法
lrc_show 的定位基于 TIME_LABEL 的 dbtime_s/btime_100ms(秒 + 百毫秒两级精度)。由于标签按时间顺序写入,LABEL_INFO 维护 dbtime_base(首标签时间)与 dbtime_limit(末标签时间)作为快速边界判断:播放时间在区间外直接返回"无歌词",区间内再按 label_id 顺序推进或回退查找。bfirst_lable/blast_lable 标志用于处理"歌词尚未开始/已结束"的边界状态,此时 lrc_show_api 返回 false,上层显示默认界面。
使用示例
示例 1:业务侧初始化与解析(本地文件)
上层(如 ui_action_music_lyrics.c 所在的播放器应用)的标准调用序列如下,lyric_init 返回的句柄贯穿整个播放会话:
void *lrc = lyric_init(); // 分配并注册歌词实例
...
FILE *lrc_file = fopen(lrc_path, "r"); // 打开与音乐同名的 .lrc
FILE *newFile = NULL;
bool ok = lrc_analysis_api(lrc, lrc_file, &newFile); // 解析,newFile 指向歌词之后的音乐数据
...
while (playing) {
u16 t_s = get_play_time_s(); // 播放器提供当前秒
u8 t_100ms = get_play_time_100ms(); // 百毫秒
if (lrc_show_api(lrc, LRC_DISPLAY_TEXT_ID, t_s, t_100ms)) {
// 歌词内容已更新到 lrc 内部 buf,刷新显示控件
}
}
...
lyric_exit(lrc);
说明:示例中的调用序列与 lyrics_api.h 声明的 API 一一对应;newFile 返回机制支持"歌词与音乐在同一个文件容器"的场景。
示例 2:缓冲配置宏(内存预算)
缓冲区大小全部以 4 字节对齐的宏形式集中定义在 lyrics_api.h,产品定制时只需改宏:
#define ONCE_READ_LENGTH ALIGN_4BYTE(128) ///<涉及内存对齐问题,值最好是4的倍数(最大允许值为255)
#define ONCE_DIS_LENGTH ALIGN_4BYTE(64) ///显示歌词的缓存长度
#define LABEL_TEMP_BUF_LEN ALIGN_4BYTE(2048) ///暂用2K缓存时间标签
Source: lyrics_api.h
ONCE_READ_LENGTH 上限 255 字节是受 LRC_INFO.read_next_lrc_flag/real_len 字段宽度约束的;ALIGN_4BYTE 保证与 DMA/文件系统块对齐,避免跨缓存边界读取。
示例 3:音频调试打点
排查音频数据流问题时,放开注释的 audio_debug 即可观察环形缓冲的读写状态:
if (rlen == 0) {
/* audio_debug("device_get_voice_data no data"); */
return 0;
} else {
/* audio_debug("device_get_voice_data %d",rlen); */
}
...
/* audio_debug("cbuf_write full"); */
Source: audio_input.c
这些打点分布在数据流的关键分支(无数据、缓冲满、写入长度不一致),配合 audio_debug.h 的日志开关即可在串口上还原音频数据的完整生命周期。
配置选项
歌词模块的配置分为三类:编译宏、初始化参数(LRC_CFG)、量产工具配置(isd_tools.cfg)。
编译宏
| 宏 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_LRC_LYRICS_ENABLE | 宏开关 | 取决于工程 | 整个歌词模块的编译开关,lyrics_api.c 整体包在 #if TCFG_LRC_LYRICS_ENABLE 内 |
TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH | 宏开关 | 0(默认关闭) | 1:时间标签保存到 Flash,兼容超长歌词;0:仅 RAM 缓存 |
LRC_DISPLAY_TEXT_ID | 宏 | DVcTxt1_11 | 歌词文本显示控件 ID |
LRC_DISPLAY_TEXT_LEN | 宏 | 32 | 单行歌词显示区长度(字节),也是滚动速度分档阈值 |
ONCE_READ_LENGTH | 宏 | ALIGN_4BYTE(128) | 单次读取歌词文件长度,须为 4 的倍数且 ≤255 |
ONCE_DIS_LENGTH | 宏 | ALIGN_4BYTE(64) | 显示歌词的缓存长度 |
LABEL_TEMP_BUF_LEN | 宏 | ALIGN_4BYTE(2048) | 时间标签临时缓存,2048 字节 ≈ 1024 个标签 |
LRC_CFG 初始化参数
| 字段 | 类型 | 默认值(lyric_init 中) | 说明 |
|---|---|---|---|
once_read_len | u16 | ONCE_READ_LENGTH | 一次读取长度 |
once_disp_len | u16 | ONCE_DIS_LENGTH | 一次显示缓存长度 |
label_temp_buf_len | u16 | LABEL_TEMP_BUF_LEN | 时间标签缓存总长度 |
lrc_text_id | u8 | 注释掉(0) | 文本 ID(显示层使用) |
read_next_lrc_flag | u8 | 0 | 是否预读下一条歌词 |
enable_save_lable_to_flash | u8 | 0(宏使能时) | 保存时间标签到 Flash |
roll_speed_ctrl_cb | 函数指针 | lrc_roll_speed_ctrl | 翻页滚动速度控制回调 |
clr_lrc_disp_cb | 函数指针 | NULL | 清屏回调 |
isd_tools.cfg(Flash 区间)
| 键 | 值 | 说明 |
|---|---|---|
LRIF_ADDR | AUTO | 时间标签 Flash 区间地址自动分配 |
LRIF_LEN | 0x10000 | 预留 64K 用于保存时间标签 |
LRIF_OPT | 1 | 使能该区间 |
API 参考
歌词业务 API(lyrics_api.h / lyrics_api.c)
void *lyric_init(void)
初始化歌词模块并创建实例。分配全部内部缓冲区(对齐后一次性 zalloc)、执行 lrc_param_init、加锁后挂入全局链表。
- 返回:
LRC_INFO *(以void *形式返回);分配或初始化失败返回NULL。 - 注意:调用前需确认
TCFG_LRC_LYRICS_ENABLE已使能。
void lyric_exit(void *lrc)
销毁歌词实例:加锁从全局链表摘除、释放内存。播放会话结束、切歌或播放器关闭时调用。
bool lrc_analysis_api(void *lrc, FILE *file, FILE **newFile)
解析本地歌词文件。
- 参数:
lrc实例句柄;file已打开的.lrc文件;newFile输出参数,指向歌词数据之后的位置(歌词与音乐同容器时用于续读音乐数据)。 - 返回:
true解析成功(标签表就绪),false失败(文件格式不支持、缓存不足等)。
bool net_lrc_analysis_api(void *lrc, void *net_lrc_file)
解析网络下载的歌词流,内部使用 net_download_read/seek 作为 IO。
- 参数:
net_lrc_file为net_download句柄(伪装为FILE *)。 - 返回:
true成功,false失败。
void lrc_set_analysis_flag(void *lrc, u8 flag)
设置解析标志(对应 LRC_INFO.analysis_flag),用于控制解析过程中的特殊行为(如强制重新解析)。
bool lrc_show_api(void *lrc, int text_id, u16 dbtime_s, u8 btime_100ms)
按播放时刻取出应显示的歌词,写入内部显示缓存并置更新标志。
- 参数:
text_id文本控件 ID;dbtime_s播放秒;btime_100ms百毫秒。 - 返回:
true表示歌词有更新(上层应刷新显示);false表示无歌词或未到更新时间(时间早于首标签/晚于末标签)。
bool lrc_get_api(void *lrc, u16 dbtime_s, u8 btime_100ms)
仅查询当前时刻歌词是否存在,不触发显示更新(预读/状态判断用)。
核心引擎 API(lyrics.h)
int lrc_param_init(LRC_CFG *cfg, LRC_INFO *lrc)
按配置初始化实例内部字段与缓冲区布局。返回 0 成功,非 0 失败。
bool lrc_analysis(LRC_INFO *lrc, void *lrc_handle, const LRC_FILE_IO *file_io)
通用解析入口,file_io 决定数据源(本地或网络)。这是 lrc_analysis_api 与 net_lrc_analysis_api 的共同内核。
bool lrc_get(LRC_INFO *lrc, u16 dbtime_s, u8 btime_100ms)
按时间定位歌词(不更新显示)。
bool lrc_show(LRC_INFO *lrc, int text_id, u16 dbtime_s, u8 btime_100ms)
按时间定位并输出歌词到显示缓存,设置 bis_lrc_update。
void lrc_destroy(LRC_INFO *lrc)
销毁引擎实例(内部资源释放,不操作全局链表——链表管理在 lyrics_api.c 层)。
失败模式、边界情况与并发
歌词解析失败
- 编码不支持:引擎只识别
LRC_GBK、LRC_UTF16_S、LRC_UTF16_B、LRC_UTF8四种编码,其他编码(如带 BOM 的变种或加密歌词)会解析失败,lrc_analysis_api返回false。上层应回退为不显示歌词,而不是阻塞播放。 - 无有效时间标签:标签表为空时,任何时刻调用
lrc_show_api都返回false;LABEL_INFO.dblabel_cnt为 0 即无标签。 - 内存不足:
lyric_init中zalloc失败返回NULL,lrc_param_init失败会释放内存并返回NULL——上层必须判空,防止空指针调用。
长歌词文件不兼容(重要边界)
默认 RAM 模式只有 LABEL_TEMP_BUF_LEN(2048 字节)缓存时间标签,超出(约 1024 个标签)即无法完整解析。解决路径只有两条:使能 TCFG_LRC_ENABLE_SAVE_LABEL_TO_FLASH 并预留 LRIF_LEN=0x10000,或接受该限制。头注释明确警告:Flash 空间不足时只能关闭该宏,代价是不兼容长歌词文件。这是资源受限嵌入式平台上的典型取舍。
并发与线程安全
lyrics_api.c 用静态 OS_MUTEX mutex 保护全局实例链表:lyric_init 的 list_add_tail 与 lyric_exit 的摘除都在 os_mutex_pend/post 临界区内,保证多线程(如多个播放器实例或 UI 线程与播放线程并发创建/销毁歌词)下链表操作安全。互斥锁通过 late_initcall(lrc_analysis_mutex_init) 在系统初始化后期创建,确保在使用前就绪。单个实例内的解析/显示操作由上层保证串行(通常同一播放会话只在一个任务中访问),引擎自身不做实例级加锁——这是为了在热路径(每帧调 lrc_show_api)上避免锁开销。
时间边界
- 播放时间早于首标签:
bfirst_lable为假,返回"未开始"; - 播放时间晚于末标签:
blast_lable为真,返回"已结束"; - 标签定位使用秒 + 百毫秒两级精度,调用方(播放器)必须提供准确的
dbtime_s/btime_100ms,否则歌词会提前/滞后。
性能与运行注意事项
- 缓冲区对齐:
ONCE_READ_LENGTH、ONCE_DIS_LENGTH、LABEL_TEMP_BUF_LEN全部ALIGN_4BYTE,且ONCE_READ_LENGTH上限 255,兼顾 DMA 传输与文件系统块对齐。 - 单次分配:
lyric_init一次zalloc划分所有内存,减少堆碎片,代价是初始化内存峰值较大(need_buf_size在启动日志中打印,可据此评估 RAM 预算)。 - 滚动速度自适应:
lrc_roll_speed_ctrl按歌词长度分三档计算翻页速度,避免长句滚动过慢导致跟不上演唱。产品若显示区宽度不是 32 字节,应重写该回调。 - 网络歌词流式解析:
net_lrc_file_io按需seek/read,不会一次性缓存整个歌词文件,内存占用恒定;但网络延迟会影响解析速度,net_download内部需保证seek定位可靠(如支持随机访问的 HTTP Range 或本地缓存)。
扩展点
- 自定义滚动速度:实现
void (*roll_speed_ctrl_cb)(u8 lrc_len, u32 time_gap, u8 *roll_speed)并传入LRC_CFG,可针对不同字号/屏宽调整滚动策略。 - 清屏回调:
clr_lrc_disp_cb在需要清除歌词显示时被调用,产品可挂接自己的清屏逻辑(如切歌瞬间擦除残影)。 - 新数据源:实现一组
LRC_FILE_IO(seek+read)即可接入任意数据源(如蓝牙歌词、AirPlay 歌词流),无需修改引擎;net_lrc_file_io就是现成范例。 - 显示控件:LVGL
jl_extra的lv_lyrics.h提供歌词控件本体,lyrics_anim_effect.c提供滚动/强调动画,产品可在其上叠加卡拉 OK 逐字高亮等效果;官方lv_example_lyrics_1~5展示了五种基础用法。 - 调试输出:
audio_debug宏按需开启;uartPcmSender把 PCM 发到上位机,二者组合可覆盖"逻辑链路"与"波形质量"两层调试需求。
测试与示例
- LVGL 官方示例:
sdk/apps/common/lvgl_v8/examples/widgets/lyrics/lv_example_lyrics_1.c~lv_example_lyrics_5.c是歌词控件的五种演示(基础显示、滚动、动画等),可作为控件 API 的用法参考。 - 控件头文件:
sdk/apps/common/lvgl_v8/src/extra/jl_extra/widgets/lyrics/lv_lyrics.h定义歌词控件的对外接口。 - 工程集成:
sdk/apps/wifi_soundbox/lvgl_v8_ui_app/style_JL/custom/ui_action_music_lyrics.c(配合lyrics_anim_effect.c/h)展示播放页歌词的实际接线;sdk/apps/wifi_camera/lvgl_v8_ui_app/style_LY/custom/ui_action_page_lyrics.c展示另一产品线的歌词页。 - 引擎自检:引擎层通过
log_info("lrc need_buf_size=%d---", need_buf_size)输出内存预算,解析/显示结果可通过lrc_analysis_api/lrc_show_api返回值在业务层断言。
相关链接
- 歌词核心结构定义:lyrics.h
- 歌词业务 API 头文件:lyrics_api.h
- 歌词业务 API 实现:lyrics_api.c
- 音频公共模块(引入调试头文件):audio_general.c
- 音频调试打点示例:audio_input.c
- LVGL 歌词控件头文件:lv_lyrics.h
- 歌词控件示例:lv_example_lyrics_1.c
- 播放页歌词集成(wifi_soundbox):ui_action_music_lyrics.c
- 歌词滚动动画:lyrics_anim_effect.c
- 歌词页集成(wifi_camera):ui_action_page_lyrics.c