资源打包
本文档介绍 iOS-JL_Health 项目中语音资源打包能力的完整实现,涵盖 JLPackageResKit 框架中资源打包(JLPackageSourceMgr)、提示音打包与解析(JLVoicePackageManager)、PCM 转 WTS 编码(JLPcmToWts)以及设备端提示音替换集成(JLTipsSoundReplaceMgr)等模块。
Purpose and Scope
本页面以「资源打包」为边界,端到端说明 JLPackageResKit 工具库的打包机制:
- 资源合并打包:将多个二进制文件(
.bin/.res)通过JLPackageSourceMgr makePks:合并为单一资源包; - 提示音打包与解析:将多个
.wts语音文件打包为tone.cfg,并可反向解析;同时定义与设备交互的JLVoiceReplaceInfo数据结构; - PCM 转 WTS 编码:
JLPcmToWts单例提供 PCM→WTS 转换,支持码率、采样率、VAD 阈值与编码策略配置; - 设备端提示音替换流程:通过
JLTipsSoundReplaceMgr查询设备支持能力、获取已有提示音信息、下发新提示音。
以下内容不属于本页面范围,请参见对应目录页:设备 BLE 通信层(JL_ManagerM、JL_BLEMultiple 的连接与命令机制)、具体业务页面(如闹钟、来电提示音 UI)、AI 云服务(语音识别/合成)。
概述
在 JL 系列健康/音频硬件生态中,设备固件内预置的提示音(如开机音、连接音、闹钟音)由语音资源文件组成。开发者常需在 App 侧完成两类工作:
- 资源归一化:把多个独立的二进制资源文件合并成一个整体包,便于一次性传输、校验和写入;
- 提示音定制:将用户自定义的
.wts语音文件打包为设备可解析的tone.cfg配置,并通过 BLE 下发,实现提示音替换。
JLPackageResKit 正是为此设计的 Objective-C 静态工具框架,以 xcframework 形式随仓库发布(见 libs/JLPackageResKit.xcframework 下的 ios-arm64 与 ios-arm64_x86_64-simulator 两个切片),通过统一头文件 JLPackageResKit.h 暴露全部公共组件。其核心设计思想是:以"打包/解包"对称的 API 形态封装二进制容器格式,让上层业务只需关心"文件 → 数据 → 设备"的语义,而无需关心字节布局。
关键概念
| 概念 | 说明 |
|---|---|
资源包(.res / package) | JLPackageSourceMgr makePks: 的输出,将多个文件条目合并后的二进制数据,可直接写盘或传输 |
tone.cfg | 提示音配置文件,由多个 .wts 语音文件打包而成,设备端据此解析出语音列表 |
| WTS | 设备支持的语音编码格式,由 JLPcmToWts 从 PCM 转换而来 |
| 提示音替换 | 通过 BLE 将自定义语音写入设备,替换固件默认提示音 |
架构
JLPackageResKit 的模块划分遵循"入口头文件 → 功能管理器 → 数据模型"三层结构:
flowchart TD
subgraph sg_Entry["入口层 (JLPackageResKit.h)"]
H["JLPackageResKit.h<br/>统一导入点"]
end
subgraph sg_Manager["管理器层 (功能封装)"]
SrcMgr["JLPackageSourceMgr<br/>资源打包管理器"]
VoiceMgr["JLVoicePackageManager<br/>提示音打包/解析管理器"]
PcmWts["JLPcmToWts<br/>PCM→WTS 编码器 (单例)"]
end
subgraph sg_Model["数据模型层 (打包/解析载体)"]
BaseInfo["JLPackageBaseInfo<br/>contentData / fileName"]
ReplaceInfo["JLVoiceReplaceInfo<br/>version / maxNum / infoArray"]
TipsVoice["JLTipsVoiceInfo<br/>index / offset / length / fileName / nickName"]
ToneCfg["JLToneCfgModel<br/>fileName / data"]
end
subgraph sg_Device["设备集成层 (JL_BLEKit)"]
BleMgr["JL_BLEMultiple / JL_ManagerM"]
TipsReplace["JLTipsSoundReplaceMgr<br/>支持查询 / 信息获取 / 语音下发"]
end
H --> SrcMgr
H --> VoiceMgr
H --> PcmWts
SrcMgr --> BaseInfo
VoiceMgr --> ReplaceInfo
VoiceMgr --> ToneCfg
ReplaceInfo --> TipsVoice
VoiceMgr -->|"tone.cfg 数据"| TipsReplace
TipsReplace --> BleMgr
PcmWts -->|"生成 .wts 文件"| VoiceMgr
设计意图说明:
- 入口层负责聚合导出,业务方只需
#import <JLPackageResKit/JLPackageResKit.h>即可使用全部能力; - 管理器层采用类方法/单例的轻量封装,
JLPackageSourceMgr与JLVoicePackageManager均为无状态静态方法(+方法),适合离线批量打包工具场景;JLPcmToWts为单例,内部持有编码器生命周期; - 数据模型层是打包格式的抽象:
JLPackageBaseInfo描述通用资源文件条目,JLVoiceReplaceInfo/JLTipsVoiceInfo描述设备提示音结构,JLToneCfgModel描述解析后的提示音条目; - 设备集成层依赖宿主工程已有的 JL_BLEKit(
JL_BLEMultiple获取当前JL_ManagerM),实现"打包 → 下发 → 查询"闭环。
框架最低支持 iOS 11.0+、Xcode 12.0+,集成方式为将 JLPackageResKit.framework 添加到 Target → General → Frameworks, Libraries, and Embedded Content 并设置为 Embed & Sign。
资源打包模块(JLPackageSourceMgr)
资源打包模块用于将多个 .bin 等二进制文件合并为一个资源包,输出可直接写入 .res 或任意扩展名文件的数据。该模块由两个类组成:
JLPackageBaseInfo — 文件条目模型
JLPackageBaseInfo 是描述单个待打包文件的基本信息类,定义于头文件 JLPackageSourceMgr.h:
/// 信息
@interface JLPackageBaseInfo : NSObject
@property (nonatomic, strong) NSData *contentData; // 文件内容的二进制数据
@property (nonatomic, copy) NSString *fileName; // 原始文件名(含扩展名)
@end
Source: JLPackageSourceMgr.h
设计意图:fileName 保留原始文件名(含扩展名)而非仅存数据,使打包后的资源包具备自描述能力——解包方(无论是本库的解析逻辑还是设备端固件)可以依据文件名识别条目用途(如 texture.res、config.res),避免依赖调用方维护外部索引表。
JLPackageSourceMgr — 打包管理器
/// 资源打包类
@interface JLPackageSourceMgr : NSObject
/// 将多个文件信息打包为单个资源包
/// - infos: 文件信息
+(NSData *)makePks:(NSArray<JLPackageBaseInfo *>*)infos;
@end
Source: JLPackageSourceMgr.h
行为说明:
- 入参
infos为JLPackageBaseInfo数组,每个元素代表一个要打包的文件; - 返回值为合并后的
NSData,可直接writeToFile:atomically:落盘或经 BLE/网络传输; - 方法为类方法,无内部状态,天然线程安全,可并发处理多组打包任务。
为什么是类方法而非实例方法? 打包操作是纯函数式转换(输入文件数组 → 输出二进制数据),不依赖实例状态。采用类方法消除了实例管理与生命周期问题,也便于在脚本化批量处理(如 CI 资源预处理)中直接调用。
典型打包控制流
flowchart LR
A["读取文件<br/>NSData dataWithContentsOfFile:"] --> B["构造 JLPackageBaseInfo<br/>设置 fileName / contentData"]
B --> C["批量构造条目数组<br/>infos"]
C --> D["JLPackageSourceMgr makePks:infos"]
D --> E["输出合并后 NSData"]
E --> F["写入 .res 文件<br/>或传输给设备"]
提示音打包与解析模块(JLVoicePackageManager)
提示音模块是资源打包能力的核心业务形态:将多个 .wts 语音文件打包为设备可解析的 tone.cfg,同时提供反向解析能力,用于读取设备中已有提示音配置。
JLToneCfgModel — 解析结果模型
JLToneCfgModel 描述单个提示音配置项,含两个字段:fileName(提示音文件名)与 data(文件数据)。它是 parsePks: 方法的返回元素类型,供上层遍历获取每个提示音的元信息与数据。
JLVoiceReplaceInfo — 设备提示音替换结构
JLVoiceReplaceInfo 定义提示音替换相关的容器结构,包含:
| 字段 | 类型 | 说明 |
|---|---|---|
version | 整型 | 结构版本号,用于设备端兼容判断 |
blockSize | 整型 | 保留字段(文档标注为保留,打包时通常置 0) |
maxNum | 整型 | 最大语音数量,即本包内提示音条数 |
fileName | NSString | 配置文件名(如 tone.cfg) |
infoArray | 数组 | 提示音详细列表,元素为 JLTipsVoiceInfo |
JLTipsVoiceInfo 描述单个提示音的定位信息:
| 字段 | 类型 | 说明 |
|---|---|---|
index | 整型 | 语音索引,标识设备上的槽位 |
offset | 整型 | 偏移地址(数据在包内的偏移) |
length | 整型 | 文件长度 |
fileName | NSString | 原始文件名 |
nickName | NSString | 昵称(展示用名称) |
设计意图:JLVoiceReplaceInfo 采用"容器头 + 条目列表"结构,offset/length 构成对语音数据的定位索引,设备端无需一次性加载全部语音即可按需读取指定 index 的语音——这对内存受限的嵌入式设备至关重要。version 字段则为未来格式演进预留了兼容通道。
JLVoicePackageManager — 打包/解析入口
@interface JLVoicePackageManager : NSObject
// 将多个 .wts 文件打包成 tone.cfg
+ (NSData *)makePks:(NSArray *)paths FileNames:(NSArray *)names Info:(JLVoiceReplaceInfo *)info;
// 解包 tone.cfg 获取语音列表
+ (NSArray<JLToneCfgModel *> *)parsePks:(NSData *)data;
@end
Source: JLVoicePackageManager.h
打包流程(makePks:FileNames:Info:):
- 调用方提供
.wts文件路径数组(paths)、对应文件名数组(names)以及承载版本/数量等元信息的JLVoiceReplaceInfo; - 管理器读取各文件数据,按
JLVoiceReplaceInfo的结构填充infoArray(计算每个条目的index、offset、length); - 序列化为
tone.cfg二进制数据返回。
解析流程(parsePks:):
- 输入
tone.cfg的NSData; - 按同样的二进制布局反向拆解,还原出
NSArray<JLToneCfgModel *>; - 每个
JLToneCfgModel携带fileName与data,上层可直接用于展示或再次下发。
打包与解析共用同一套格式定义,保证"打包产物可被本库解析、可被设备固件解析"的一致性。
PCM 转 WTS 模块(JLPcmToWts)
JLPcmToWts 提供 PCM 音频到设备专用 WTS 格式的编码能力,是提示音打包链路的"上游"——用户提供的音频需先转为 .wts,才能进入 JLVoicePackageManager 打包为 tone.cfg。
接口形态
@interface JLPcmToWts : NSObject
+ (instancetype)share; // 获取单例对象
- (void)pcmToWts:...; // 将 PCM 文件编码为 WTS 文件
@end
Source: JLPackageResKit.h(
JLPcmToWts.h由统一入口导入)
编码参数
| 参数 | 说明 |
|---|---|
speechInFileName | 输入 PCM 文件路径 |
bitOutFileName | 输出 WTS 文件路径 |
targetRate | 目标码率 |
sr_in | 输入采样率 |
vadthr | VAD(语音活动检测)阈值 |
usesavemodef | 编码策略:0 = 位流优先,1 = 质量优先 |
设计意图:编码参数全部显式暴露而非硬编码,是因为不同设备型号的存储容量与解码能力差异较大——低端设备适合低码率位流优先(usesavemodef = 0)以缩小文件体积,高端设备可用质量优先(usesavemodef = 1)换取更佳听感。vadthr 则用于静音段检测,剔除提示音前后的静音噪声,进一步压缩包体。
音频预处理链路
flowchart TD
A["原始音频 (PCM)"] --> B["JLPcmToWts share 单例"]
B --> C{"pcmToWts 编码"}
C -->|"usesavemodef=0 位流优先"| D["小体积 WTS"]
C -->|"usesavemodef=1 质量优先"| E["高质量 WTS"]
D --> F["JLVoicePackageManager makePks"]
E --> F
F --> G["tone.cfg 资源包"]
G --> H["JLTipsSoundReplaceMgr 下发设备"]
设备端提示音替换集成(JLTipsSoundReplaceMgr)
打包后的 tone.cfg 最终要下发到设备。该能力由 JL_BLEKit 侧的管理器 JLTipsSoundReplaceMgr(单例 share)承担,与 JLPackageResKit 的数据结构无缝衔接:
- 能力查询:
isSupportTipsVoiceReplace:result:异步回调BOOL,判断当前连接设备是否支持提示音替换; - 信息获取:
voicesReplaceGetVoiceInfo:Result:从设备读取已有提示音配置,返回NSData后可交给[JLVoiceReplaceInfo parsePks:data]解析出infoArray,逐条读取index/fileName/nickName; - 语音下发:将本地打包好的提示音数据经 BLE 命令写入设备,替换对应
index槽位。
设备管理器实例通过 [[JL_BLEMultiple share] getCurrentManager] 获取,与仓库中 BLE 通信层(JL_ManagerM)解耦——资源打包层只产出数据,不关心传输细节。
核心流程
端到端提示音定制时序
sequenceDiagram
participant App as App 业务层
participant Pcm as JLPcmToWts
participant Vpkg as JLVoicePackageManager
participant Rep as JLTipsSoundReplaceMgr
participant Ble as JL_BLEMultiple / JL_ManagerM
participant Dev as 设备固件
App->>Pcm: pcmToWts (码率/采样率/VAD/策略)
Pcm-->>App: 生成 .wts 文件
App->>Vpkg: makePks:FileNames:Info: (paths, names, JLVoiceReplaceInfo)
Vpkg-->>App: tone.cfg NSData
App->>Ble: getCurrentManager 获取设备管理器
App->>Rep: isSupportTipsVoiceReplace 能力查询
Rep-->>App: support = YES
App->>Rep: voicesReplaceGetVoiceInfo 读取已有配置
Rep->>Ble: BLE 读取命令
Ble-->>Rep: 设备返回 NSData
Rep-->>App: data
App->>App: [JLVoiceReplaceInfo parsePks:data] 解析
App->>Rep: 下发打包后的提示音数据
Rep->>Ble: BLE 写入命令
Ble->>Dev: 写入 tone.cfg / 语音数据
Dev-->>App: 替换完成
流程要点:
- 打包与解析严格对称:
makePks:产生的tone.cfg既可由本库parsePks:解析(用于调试/预览),也可由设备固件解析(用于实际替换); - 所有设备交互均为异步回调风格(
result:/Result:block),符合 BLE 命令的串行应答模型; index是设备端槽位标识,App 侧需先查询已有配置(voicesReplaceGetVoiceInfo)再决定覆盖哪个槽位,避免误替换。
使用示例
以下示例均提取自仓库文档 docs/JLPackageResKit.md。
示例 1:通用资源打包(.bin → .res)
// 准备要打包的文件
JLPackageBaseInfo *file1 = [JLPackageBaseInfo new];
file1.fileName = @"texture.res";
file1.contentData = [NSData dataWithContentsOfFile:path1];
JLPackageBaseInfo *file2 = [JLPackageBaseInfo new];
file2.fileName = @"config.res";
file2.contentData = [NSData dataWithContentsOfFile:path2];
// 执行打包
NSData *packageData = [JLPackageSourceMgr makePks:@[file1, file2]];
// 写入文件
[packageData writeToFile:@"/path/to/package" atomically:YES];
Source: JLPackageResKit.md
说明:先逐文件读取为 NSData 并设置 fileName,再一次性打包。该模式适合资源管理器(如图片/配置/字体)的批量归并。
示例 2:提示音打包为 tone.cfg
//1. 打包多个 .wts 文件为 tone.cfg
NSArray *paths = @[
@"/Users/xxx/voice1.wts",
@"/Users/xxx/voice2.wts"
];
NSArray *names = @[
@"boot.wts",
@"connect.wts"
];
JLVoiceReplaceInfo *info = [[JLVoiceReplaceInfo alloc] init];
info.version = 1;
info.fileName = @"tone.cfg";
info.maxNum = 2;
info.blockSize = 0;
NSData *cfgData = [JLVoicePackageManager makePks:paths FileNames:names Info:info];
// 将生成的 tone.cfg 写入本地
[cfgData writeToFile:@"/Users/xxx/tone.cfg" atomically:YES];
Source: JLPackageResKit.md
说明:paths 与 names 数组需一一对应;info.maxNum 应等于语音条数(此处为 2),version 用于设备端格式兼容判断。
示例 3:解析 tone.cfg
//2. 解析 tone.cfg 文件
NSData *data = [NSData dataWithContentsOfFile:@"/Users/xxx/tone.cfg"];
NSArray<JLToneCfgModel *> *tones = [JLVoicePackageManager parsePks:data];
for (JLToneCfgModel *model in tones) {
NSLog(@"Tone File: %@, Data Length: %lu", model.fileName, (unsigned long)model.data.length);
}
Source: JLPackageResKit.md
示例 4:查询设备支持并获取已有提示音
//3. 查询设备是否支持提示音替换
//这里假设是通过某个初始化的类获取到了 manager 类,具体实现方式需要用户根据实际调整
JL_ManagerM *manager = [[JL_BLEMultiple share] getCurrentManager];
[[JLTipsSoundReplaceMgr share] isSupportTipsVoiceReplace:manager result:^(BOOL support) {
if (support) {
NSLog(@"支持提示音替换");
} else {
NSLog(@"不支持提示音替换");
}
}];
//4. 获取设备中已有的提示音信息
[[JLTipsSoundReplaceMgr share] voicesReplaceGetVoiceInfo:manager Result:^(JL_CMDStatus status, NSData * _Nullable data) {
if (status == JL_CMDStatusSuccess && data) {
JLVoiceReplaceInfo *info = [JLVoiceReplaceInfo parsePks:data];
for (JLTipsVoiceInfo *item in info.infoArray) {
NSLog(@"Index: %d, Name: %@ (%@)", item.index, item.fileName, item.nickName);
}
} else {
NSLog(@"获取失败");
}
}];
Source: JLPackageResKit.md
说明:设备返回的原始 NSData 复用 JLVoiceReplaceInfo parsePks: 解析,说明打包格式同时服务于"文件 ↔ 数据"与"设备 ↔ App"两条链路。
API 参考
JLPackageSourceMgr
| 方法 | 签名 | 说明 |
|---|---|---|
makePks: | +(NSData *)makePks:(NSArray<JLPackageBaseInfo *> *)infos | 将多个文件条目打包为单个资源包。参数 infos 为待打包文件信息数组;返回打包后的 NSData,可直接写入 .res 文件 |
JLPackageBaseInfo
| 属性 | 类型 | 说明 |
|---|---|---|
contentData | NSData * | 文件内容的二进制数据表示 |
fileName | NSString * | 原始文件名(包含扩展名) |
JLVoicePackageManager
| 方法 | 签名 | 说明 |
|---|---|---|
makePks:FileNames:Info: | +(NSData *)makePks:(NSArray *)paths FileNames:(NSArray *)names Info:(JLVoiceReplaceInfo *)info | 将多个 .wts 文件打包成 tone.cfg。paths 为文件路径数组,names 为对应文件名数组,info 承载版本/数量等元信息 |
parsePks: | +(NSArray<JLToneCfgModel *> *)parsePks:(NSData *)data | 解包 tone.cfg 数据,返回语音条目列表 |
JLPcmToWts
| 方法 | 签名 | 说明 |
|---|---|---|
share | +(instancetype)share | 获取单例对象 |
pcmToWts:... | -(void)pcmToWts:... | 将 PCM 文件编码为 WTS 文件;参数覆盖输入/输出路径、目标码率、采样率、VAD 阈值、编码策略(0 位流优先 / 1 质量优先) |
JLVoiceReplaceInfo
| 方法/属性 | 说明 |
|---|---|
parsePks: | 从设备返回或本地文件解析出 JLVoiceReplaceInfo |
version / blockSize / maxNum / fileName / infoArray | 容器头元信息与条目列表(元素为 JLTipsVoiceInfo) |
设备侧(JL_BLEKit 集成)
| 方法 | 说明 |
|---|---|
isSupportTipsVoiceReplace:result: | 查询设备是否支持提示音替换,异步回调 BOOL |
voicesReplaceGetVoiceInfo:Result: | 读取设备已有提示音配置,回调 JL_CMDStatus 与原始 NSData |
故障模式与边界情况
- 文件读取失败:
NSData dataWithContentsOfFile:返回nil时,条目数据为空,打包结果可能不完整。建议打包前校验每个文件的读取结果与fileName非空。 - 路径与名称数组不一致:
makePks:FileNames:Info:要求paths与names一一对应;若数组长度不等,可能导致条目错位或越界,调用方需保证长度一致。 maxNum与实际条数不符:JLVoiceReplaceInfo.maxNum应等于实际语音数量;设备端可能按maxNum分配槽位,超出部分可能被忽略。- 设备不支持提示音替换:
isSupportTipsVoiceReplace:result:返回NO时,后续获取/下发命令应被跳过,避免无效 BLE 命令。 - BLE 命令失败:
voicesReplaceGetVoiceInfo:Result:回调中status != JL_CMDStatusSuccess或data == nil时,应按"获取失败"处理(示例中即如此分支)。 - 并发打包:
JLPackageSourceMgr/JLVoicePackageManager为无状态类方法,可安全并发;但JLPcmToWts为单例,编码过程若被多线程并发调用,需外部加锁或串行化,避免编码器内部状态竞争。
性能与运维考虑
- 编码是耗时操作:
pcmToWts涉及 PCM 读取、VAD 检测与位流编码,建议在后台队列执行,避免阻塞主线程;批量转码时逐文件串行或受限并发,防止内存峰值。 - 打包数据量:
makePks:会将全部文件数据载入内存合并,超大资源集合建议分批打包或控制单包体积,防止内存暴涨。 - 调试技巧:利用打包/解析对称性——对
makePks:产物立即执行parsePks:,可离线验证格式正确性,无需真实设备即可定位问题。 - 版本管理:
JLVoiceReplaceInfo.version与JLTipsVoiceInfo的布局一旦发布即成为设备端契约,后续变更需增加版本分支,保证旧设备兼容。
扩展点
- 自定义资源包格式:
JLPackageBaseInfo+JLPackageSourceMgr是通用容器,可扩展用于任意二进制资源(图片、字体、配置文件)的归并传输; - 新编码策略:
pcmToWts的usesavemodef位流/质量策略为编码器开关,未来可扩展更多档位而不改变调用方接口; - 多设备下发:
JLTipsSoundReplaceMgr与JL_ManagerM解耦,接入不同设备型号时仅需调整 BLE 命令适配层,打包层无需改动。