1. 离线语音识别服务在AOSP中的集成实践
最近在折腾AOSP源码时,发现系统自带的语音识别功能总是需要联网,这显然不符合某些特殊场景的需求。于是花了三天时间研究如何在Android系统层集成离线语音识别能力,期间踩了不少坑,也发现Vosk这个开源方案确实香。下面就把整个改造过程记录下来,特别会重点说明Android.bp文件的编写技巧——这个在官方文档里几乎找不到完整案例。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Vosk引擎的选择与移植
2.1 为什么选择Vosk
对比了多个开源方案后,Vosk的以下特性让我最终拍板:
- 支持20+种语言模型(实测中文识别准确率约92%)
- 模型文件最小可压缩到50MB左右
- Apache 2.0许可证无商业风险
- 提供完整的Android示例代码
但官方提供的aar包无法直接用于系统级集成,需要自己编译so库。这里有个关键点:必须使用NDK r21d版本编译,否则会出现奇怪的符号冲突。
2.2 模型文件处理技巧
下载的zh-cn模型原始大小有1.2GB,通过以下手段优化:
bash复制# 使用官方提供的压缩工具
python3 vosk_compress_model.py zh-cn-model/ compressed-model/
# 移除不需要的英文相关数据
rm -rf compressed-model/extra/english*
最终模型大小降至217MB,识别精度损失不到3%。建议将模型放在/system/vendor/vosk/目录下,记得在device.mk中添加PRODUCT_COPY_FILES规则。
3. RecognitionService的系统级实现
3.1 继承RecognitionService要点
系统级服务与普通APP开发的最大区别在于权限控制。核心实现类需要这样声明:
java复制public class OfflineSpeechService extends RecognitionService {
private static final String REQUIRED_PERMISSION = "android.permission.MANAGE_VOICE_KEYPHRASES";
@Override
protected void onStartListening(Intent recognizerIntent, Callback callback) {
enforceCallingPermission(REQUIRED_PERMISSION);
// 实际识别逻辑
}
}
特别注意:必须添加SELinux策略,否则服务会被拒绝启动。建议在service_contexts中添加:
code复制offline_speech_service u:object_r:system_server_service:s0
3.2 音频管道配置
实测发现直接使用AudioRecord会出现400ms左右的延迟。优化方案是重写AudioFlinger的配置:
cpp复制// 在audio_policy_configuration.xml中添加
<module name="primary" halVersion="3.0">
<attachedDevices>
<item>Speaker</item>
<item>Built-In Mic</item>
</attachedDevices>
<defaultOutputDevice>Speaker</defaultOutputDevice>
<mixPorts>
<mixPort name="voice_trigger" role="source" flags="AUDIO_INPUT_FLAG_VOIP_TX">
<profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
samplingRates="16000" channelMasks="AUDIO_CHANNEL_IN_MONO"/>
</mixPort>
</mixPorts>
</module>
4. Android.bp的深度配置
4.1 原生库的编译陷阱
Vosk的Kaldi依赖需要特殊处理,否则会链接失败。关键配置如下:
bp复制cc_library_shared {
name: "libvosk_jni",
srcs: ["src/main/jni/**/*.cpp"],
shared_libs: [
"liblog",
"libandroid",
],
static_libs: [
"libkaldi-online2",
"libkaldi-feat",
],
cflags: [
"-DHAVE_CUDA=0",
"-DHAVE_OPENBLAS=1",
"-mfpu=neon", // 必须添加ARM NEON优化
],
ldflags: ["-Wl,--exclude-libs=libgcc.a"],
}
4.2 资源文件的打包技巧
模型文件需要通过特殊方式打包进系统镜像:
bp复制prebuilt_etc {
name: "vosk_zh_model",
src: "compressed-model/",
sub_dir: "vosk",
filename_from_src: true,
required: ["libvosk_jni"],
}
这里有个坑:如果模型文件数量超过100个,需要先打包成tar再解压。我写了个python脚本自动处理:
python复制# 在Android.bp中调用的预处理脚本
def package_large_model():
os.system("tar -cf model.tar compressed-model/*")
return "model.tar"
5. 性能优化实战记录
5.1 内存泄漏排查
使用Android Studio的Profiler发现每次识别会泄漏约200KB内存。根本原因是Kaldi的OnlineEndpointRule没有正确释放。修复方案:
diff复制+class AutoEndpoint : public OnlineEndpointRule {
+public:
+ ~AutoEndpoint() {
+ delete rule;
+ }
+};
5.2 冷启动加速
首次加载模型需要8秒左右,通过以下优化降至1.3秒:
- 使用mmap加载模型文件
- 预加载常用词库
- 启用Zygote预加载
关键代码段:
cpp复制void* model_ptr = mmap(NULL, model_size, PROT_READ, MAP_PRIVATE, fd, 0);
mlock(model_ptr, model_size); // 防止被swap
6. 实际效果验证
在Pixel 3上测试结果:
- 中文短句识别准确率:91.7%
- 平均响应延迟:280ms
- CPU占用率:12-15%
- 内存占用:稳定在78MB左右
测试时发现一个有趣现象:在系统启动阶段调用服务会出现初始化失败。后来发现是依赖的audiohal服务还没ready。解决方法是在init.rc中添加:
code复制# 等待音频服务就绪
exec_background - system system -- /system/bin/sleep 5
start offline_speech_service
这个项目最深的体会是:系统级开发就像在瓷器店里打拳击,每个细节都可能引发连锁反应。特别是Android.bp的配置,稍有偏差就会导致编译通过但运行时崩溃。建议每次修改后都做完整的CTS验证,我就是在测试过程中发现了三个潜在的SELinux违规。
