web-to-app 背景音乐(BGM)功能详解:播放模式、LRC 同步歌词与 APK 加密打包
【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app
本文基于 web-to-app 仓库的 BGM 官方文档 与对应源码,系统讲解如何在生成的应用中配置带同步歌词的背景音乐:从「编辑通用配置」中的 BGM 卡片各选项,到BgmConfig数据模型、BgmPlayer播放引擎的三种播放模式实现、LRC 歌词的解析与持久化规则,再到音频文件打包进导出 APK 时的加密与解密链路。读完本文,你可以完整掌握该功能的配置方法、参数含义与底层实现机制。
BGM 卡片的位置与核心选项
BGM 功能入口位于 编辑通用配置编辑器 中的背景音乐卡片,用于在生成的应用里播放背景音乐并显示同步歌词。卡片对应 UI 实现为 BgmCard.kt。文档定义了五个选项:
- 启用—— 打开背景音乐,对应数据模型中的
bgmEnabled字段(见 WebApp.kt#L107-L108,默认false)。 - 播放列表—— 添加音乐曲目;支持同步LRC 歌词与歌词动画。
- 播放模式—— 循环、顺序或随机(
BgmPlayMode)。随机模式首曲即随机,每轮每首播一次不重复,播完重洗。 - 歌词样式—— 自定义字体、颜色、描边和阴影。
- 在线搜索—— 在线搜索音乐。
数据模型:BgmConfig、BgmItem与播放模式
所有配置收敛在 WebApp.kt 中定义的一组数据类上。WebApp应用模型通过bgmEnabled(是否启用)与bgmConfig(具体配置)两个字段持有 BGM 状态。
BgmConfig:播放器配置
定义见 WebApp.kt#L1162-L1169,字段与默认值如下:
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
playlist | List<BgmItem> | 空列表 | 播放列表,为空时播放器直接返回不播放 |
playMode | BgmPlayMode | LOOP | 循环 / 顺序 / 随机 |
volume | Float | 0.5f | 音量,取值 0~1 |
autoPlay | Boolean | true | 初始化后是否自动开唱首曲 |
showLyrics | Boolean | true | 是否显示歌词 |
lrcTheme | LrcTheme? | null | 歌词样式主题 |
BgmItem:曲库条目
BgmItem(WebApp.kt#L1149-L1160)描述单首曲目:name(曲目名)、path(音频路径)、coverPath(封面,可选)、isAsset(是否为 assets 预置曲目)、tags(曲风标签)、lrcData(解析后的歌词)等。
path的取值有两种形态,这也是后续解密链路分叉的根源:
asset:///bgm/xxx.mp3—— 预置在 APK assets 中的音乐;- 绝对路径(如
/data/user/0/.../files/bgm/xxx.mp3)—— 用户导入的音乐文件。
BgmPlayMode与随机模式的「洗牌」语义
BgmPlayMode枚举包含LOOP、SEQUENTIAL、SHUFFLE三个值(WebApp.kt#L1042-L1046)。文档中「随机模式首曲即随机,每轮每首播一次不重复,播完重洗」的完整实现在 BgmPlayer.kt 中:
- 首曲即随机:
initialize()在配置为SHUFFLE时立即生成shuffledIndices = playlist.indices.shuffled()(BgmPlayer.kt#L67-L69),因此第一轮第一首就是随机抽中的; - 每轮不重复:播放索引
currentIndex在洗牌数组上顺序前进,playCurrentTrack()通过shuffledIndices[currentIndex]映射回真实曲目(BgmPlayer.kt#L80-L83),保证一轮内不重复; - 播完重洗:
playNext()中索引取模回绕到 0 时重新shuffled()(BgmPlayer.kt#L153-L164),开启新一轮洗牌。
另外两种模式的细节:
- LOOP(循环):单曲列表时直接置
isLooping = true交给MediaPlayer硬件级循环;多曲时每首播完调用playNext()轮转(BgmPlayer.kt#L101、BgmPlayer.kt#L127-L135)。 - SEQUENTIAL(顺序):按列表顺序播放,末曲播完后回到第一首继续(BgmPlayer.kt#L136-L145)。
进度上报由主线程Handler每 100ms 轮询MediaPlayer.currentPosition并回调onProgressListener(BgmPlayer.kt#L33-L47),供歌词高亮与进度条使用。
LRC 歌词:格式解析、旁挂持久化与样式
解析规则
LRC 文本解析在 BgmStorage.parseLrcText() 中完成,使用两条正则:
- 时间戳行:
[mm:ss.xx]歌词,毫秒支持两位或三位((\d{2,3})); - 元数据行:
[ti:标题]、[ar:歌手]、[al:专辑](大小写不敏感)。
每行解析为LrcLine(startTime, endTime, text, translation?)。endTime的推断策略是:先默认给 5000ms 的占位时长,随后统一把每一行的endTime修正为下一行的startTime(BgmStorage.kt#L491-L493),即「当前句持续到下一句开始」,这正是歌词高亮换行的时间依据。LrcLine还带可选的translation字段,支撑歌词翻译场景。
旁挂(sidecar)持久化
文档强调:歌词和标签的修改会持久化到曲库(旁挂.lrc与标签文件),即使曲目尚未保存进应用配置,刷新和重启后依然保留。对应实现:
.lrc旁挂文件路径由 getLrcPathForBgm() 统一约定:用户音乐与其同目录同名的.lrc;assets 预置音乐因为 assets 只读,改写为写入filesDir/bgm/<同名>.lrc,扫描时优先采用该用户覆盖版本(BgmStorage.kt#L81-L93)。- 保存歌词时通过 saveLrc() 重写标准 LRC 文本,其中翻译行以「同一时间戳双行」形式写出,注释说明这与导出管线(
ApkBuilder.convertLrcDataToLrcString)的 wire 格式保持一致,保证翻译在旁挂文件往返中不丢失。 - 曲风标签(
BgmTag,如PURE_MUSIC、ANIME、SLEEP等,见 WebApp.kt#L1048-L1092)不随曲目配置保存,而是集中持久化在曲库级文件bgm/library_tags.json中,键为asset/<曲目名>或user/<曲目名>(saveTagsForBgm())。源码注释解释了这样设计的原因:否则标签只存在于恰好引用了该曲目的应用配置里,重新扫描就会丢失。 - 歌词的编辑与手动对齐入口在 UI 层的 LrcEditorDialog.kt 与 ManualLrcAligner.kt。
歌词样式:LrcTheme
文档中的「歌词样式 —— 自定义字体、颜色、描边和阴影」对应 LrcTheme:
| 字段 | 默认值 | 含义 |
|---|---|---|
fontFamily | "default" | 字体 |
fontSize | 18f | 字号 |
textColor | #FFFFFF | 普通歌词颜色 |
highlightColor | #FFD700 | 当前句高亮颜色 |
backgroundColor | #80000000 | 背景色(半透明黑) |
strokeColor/strokeWidth | null/0f | 描边颜色与宽度 |
shadowEnabled | true | 是否启用阴影 |
animationType | FADE | 歌词动画类型 |
position | BOTTOM | 歌词位置(TOP/CENTER/BOTTOM) |
showTranslation | true | 是否显示翻译 |
歌词动画类型枚举 LrcAnimationType 提供 7 种:NONE、FADE、SLIDE_UP、SLIDE_LEFT、SCALE、TYPEWRITER(打字机)、KARAOKE(卡拉 OK 逐字高亮),即文档所说的「歌词动画」。
曲库扫描:assets 预置与用户导入
曲库扫描由 BgmStorage.scanAllBgm() 完成,合并两个来源:
- assets 预置曲库:
app/src/main/assets/bgm/目录。其 README.txt 定义了预置文件的命名规则:- 音乐文件:
小乔.mp3; - 封面图片:
小乔.png(或.jpg/.jpeg); - 同名音乐与图片自动配对,图片作为封面图标显示;封面可选,仅用于选择界面识别;
- 示例:
小乔.mp3 + 小乔.png、背景音乐1.mp3 + 背景音乐1.jpg、bgm_01.mp3(无封面)。
- 音乐文件:
- 用户曲库:应用私有目录
filesDir/bgm/,通过 scanUserBgm() 扫描,封面与 LRC 均按同名旁挂匹配。
两个扫描器对音频格式的约定一致,即 MUSIC_EXTENSIONS:mp3、m4a、aac、ogg、flac、wav—— 与文档「在线音乐搜索按真实格式下载曲目 —— MP3、M4A、AAC、OGG、FLAC 或 WAV —— 都会出现在选择器中」的描述完全吻合。封面识别扩展名为png/jpg/jpeg/jpe/jfif/webp/bmp/gif/heic/heif,且jpeg/jpe/jfif会规范化为jpg(normalizeCoverExtension())。
用户从系统导入音频时,saveBgm() 会把 ContentResolver 中的 URI 拷贝为bgmDir/<安全文件名>.mp3:文件名经sanitizeBgmName()清洗(仅保留字母、数字、中文、_、-,空白时回退为bgm_<UUID>),并在写入后校验文件非空。封面保存 saveCover() 会先删除旧扩展名的同名文件再写入,扩展名解析顺序为 MIME → URI 路径提示 → 文件头魔数(支持识别 JPEG/PNG/GIF/WEBP/BMP 头),最终兜底jpg。
选择器 UI 实现在 BgmSelector.kt,运行时展示则挂载在壳应用界面(ShellBgmPlayer.kt),由 ShellScreen.kt 等壳组件装配。
在线音乐搜索
「在线搜索」选项的 API 与下载逻辑位于core/bgm包下的 OnlineMusicApi.kt 与 OnlineMusicDownloader.kt,UI 入口为 OnlineMusicSearchDialog.kt。下载的曲目按真实音频格式落盘,只要扩展名在上述MUSIC_EXTENSIONS六者之列,就会在下次曲库扫描中进入选择器——这就是文档中「按真实格式下载曲目都会出现在选择器中」的保证机制。
BGM 在导出 APK 中的打包与加密
文档说明「BGM 音频文件被打包进导出的 APK(并可加密)」,这条链路在 BgmPlayer.setAssetDataSource() 中完整可见:
- 播放
asset:///路径的曲目时,先探测是否存在<assetPath>.enc加密副本; - 有加密副本:通过
AssetDecryptor.loadAsset()解密为字节流,写入cacheDir下临时文件(命名bgm_<hash>.mp3),随后用本地文件路径喂给MediaPlayer。临时文件缓存在tempFileCache中,同一曲目反复切歌不再重复解密;release()时统一清理临时文件(BgmPlayer.kt#L315-L337); - 无加密副本:直接
assets.openFd()获取AssetFileDescriptor,以 fd + 偏移 + 长度方式设置数据源,避免整文件读入内存。
用户曲目(非 asset 路径)则直接setDataSource(绝对路径)播放,不参与 assets 打包。
另外,bgmEnabled还会影响导出 APK 的运行时权限集合:RuntimePermissionSync.kt#L88 处按「若bgmEnabled则追加相应权限」的逻辑参与权限同步,保证生成应用具备播放所需的系统权限。
小结
web-to-app 的 BGM 功能是一条完整的「配置 → 曲库 → 播放 → 导出」链路:
- 配置侧:
bgmEnabled开关 +BgmConfig(播放列表、BgmPlayMode、音量、自动播放、歌词显示、LrcTheme样式); - 曲库侧:assets 预置(
app/src/main/assets/bgm/,同名配对封面)与用户目录filesDir/bgm/双来源扫描,支持六种音频格式、多格式封面识别、LRC 旁挂与library_tags.json标签持久化,刷新和重启后修改不丢失; - 播放侧:
BgmPlayer基于MediaPlayer实现循环 / 顺序 / 洗牌随机三种模式,随机模式保证「首曲随机、每轮不重复、播完重洗」; - 导出侧:音频打入 APK assets 并支持
.enc加密,运行时解密到缓存播放;
关键源码入口一览:BgmPlayer.kt、BgmStorage.kt、WebApp.kt(BGM 数据模型)、BgmCard.kt、assets/bgm 预置说明。
【免费下载链接】web-to-appThe most full featured web-to-app toolkit on Android, a complete APK workshop that runs entirely on your phone项目地址: https://gitcode.com/GitHub_Trending/web/web-to-app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考