Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障
Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障 项目概述 这次把产品里的 Snowboy 语音唤醒部分抽成了独立项目: GitHub:<https://github.com/deng weihong/snowboy kws 当前提交:\ ec24110\ 目标平台:Rockchip
Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障
项目概述
这次把产品里的 Snowboy 语音唤醒部分抽成了独立项目:
当前提交:
ec24110目标平台:Rockchip RV1106,ARMv7-A,Cortex-A7,uClibc,hard-float,NEON/VFPv4
构建系统:CMake
主要入口:
snowboy_kws::Detector示例程序:
examples/file_detector.cpp
这个仓库只负责唤醒推理。音频采集、线程调度、ALSA、IPC 和业务层由调用方负责,因此可以嵌入不同的嵌入式产品。
个人唤醒词模型没有放进仓库。Snowboy 引擎的许可证和个人模型的许可证是两件事,模型需要单独确认使用和再分发权限。
一、唤醒原理
1. 输入音频契约
Snowboy 检测器需要:
采样率:16 kHz
声道:单声道
格式:有符号 16 位 PCM,也就是 S16_LE
输入方式:连续的小块音频
每块样本数:项目适配器限制为 1 到 4096 个样本
实际产品中常用 1600 个样本作为一个块,正好是 100 ms。块越大,函数调用次数越少,但触发延迟也会增加。
2. 适配器的生命周期
Detector 构造时加载两个文件:
common.res:Snowboy 的公共资源文件,包含特征处理和检测网络运行所需的资源。.pmdl或.umdl:具体的个人模型或通用模型,决定要检测的唤醒词。
构造函数随后设置:
灵敏度
SetSensitivity输入增益
SetAudioGain是否启用 Snowboy 内置前端
ApplyFrontend
适配器会检查 Snowboy 返回的音频契约是否确实为 16 kHz、单声道、16 位。如果资源或模型加载失败,调用方可以通过 valid() 和 error() 得到原因。
3. 连续检测流程
flowchart LR
A[麦克风或音频服务] --> B[16 kHz mono S16 PCM]
B --> C[Detector::accept]
C --> D[SnowboyDetect::RunDetection]
D --> E{-2 静音 / 0 无事件}
D --> F{1..N 唤醒词触发}
F --> G[业务层发出唤醒事件]
RunDetection 的返回值含义是:
-2:静音-1:输入错误或检测失败0:没有事件1、2等:第几个热词模型被触发
当前适配器将大于 0 的返回值转成 Detection{detected=true, result=index},但不在库内部发事件,也不负责播放提示音。这样可以让业务层决定冷却时间、重复触发处理和事件协议。
4. 为什么要和音频采集解耦
最初的 Snowboy C Demo 直接使用 PortAudio 打开麦克风。产品接入后,音频服务已经负责:
打开 ALSA 设备
处理双麦同步
进行下混或预处理
把 PCM 分发给不同消费者
因此产品适配器只接受 PCM。这样做有三个好处:
不会因为两个进程同时打开麦克风而互相抢占设备。
检测库可以在离线 WAV、测试程序和产品音频服务中复用。
编译时不需要把 PortAudio、ALSA 和设备配置带进唤醒库。
二、独立仓库里的组成
snowboy-kws/
├── include/snowboy_kws/detector.h # 稳定的 C++ 适配器接口
├── src/detector.cpp # SnowboyDetect 封装
├── examples/file_detector.cpp # WAV/裸 PCM 文件示例
├── cmake/toolchains/armv7-uclibc.cmake # ARMv7/uClibc 工具链
├── scripts/build-armv7-uclibc.sh # 一键交叉编译
├── third_party/snowboy/ # 头文件、静态库、common.res
├── third_party/openblas/ # ARMv7 OpenBLAS 静态库
└── models/README.md # 用户自备模型说明
Snowboy 本体是预编译的 ARM 静态库,仓库没有引入 Snowboy 检测引擎的完整源代码。链接关系是:
snowboy_kws adapter
├── libsnowboy-detect.a
├── libopenblas_armv7-r0.3.21.a
├── libpthread
├── libdl
└── libm
Snowboy 静态库会调用 CBLAS 接口,例如 cblas_sgemm、cblas_sgemv 和 cblas_sdot,所以只提供 Snowboy 静态库还不够,必须同时提供 ABI 兼容的 OpenBLAS。
三、ARM/uClibc 交叉编译
1. 工具链必须匹配目标 ABI
本次 RV1106 使用的工具链前缀是:
arm-rockchip830-linux-uclibcgnueabihf-
其中:
arm:目标是 32 位 ARMrockchip830:工具链面向 Rockchip 830 系列平台linux-uclibc:目标 C 运行库是 uClibcgnueabihf:使用 GNU ABI 和硬浮点调用约定
编译选项必须和已经编译好的 Snowboy/OpenBLAS 静态库以及目标板一致:
-march=armv7-a
-mtune=cortex-a7
-mfpu=neon-vfpv4
-mfloat-abi=hard
如果把 hard-float 库和 soft-float 程序混用,通常会在链接阶段报 ABI 不兼容;即使侥幸链接成功,也可能在运行时出现不可预测的问题。
2. C++ ABI 必须保持一致
仓库附带的 Snowboy 静态库使用旧版 libstdc++ ABI。适配器因此显式设置:
target_compile_definitions(snowboy_kws PRIVATE _GLIBCXX_USE_CXX11_ABI=0)
如果省略这个定义,常见结果是 std::__cxx11 相关的未定义符号,或者使用 std::string 的接口无法链接。
3. sysroot 不能只看编译器名字
交叉编译器能找到,不代表目标环境完整。sysroot 至少需要提供目标侧的:
uClibc
libstdc++
libgcc
pthread
libm
libdl
相关头文件和链接脚本
示例构建:
export CROSS_COMPILE=/opt/toolchain/bin/arm-rockchip830-linux-uclibcgnueabihf-
export SYSROOT=/opt/buildroot/host/arm-buildroot-linux-uclibcgnueabihf/sysroot
./scripts/build-armv7-uclibc.sh
脚本会调用 CMake 工具链文件,设置 ARMv7 编译选项,并在设置 SYSROOT 时限制 CMake 只从目标根文件系统查找头文件和库。
4. 静态库和 OpenBLAS 的链接问题
Snowboy 本体是静态库,OpenBLAS 也是静态库。静态库不会自动把自己的依赖传递给最终链接器,所以最终程序必须显式链接:
libsnowboy-detect.a
libopenblas_armv7-r0.3.21.a
-lpthread -ldl -lm
链接顺序也很重要:通常先放使用符号的 Snowboy 库,再放提供 CBLAS 符号的 OpenBLAS 库。否则可能看到大量:
undefined reference to cblas_sgemm
undefined reference to cblas_sgemv
5. 交叉编译结果验证
不要只看编译器退出码。至少检查:
file build/armv7-uclibc/snowboy_kws_file
readelf -A build/armv7-uclibc/snowboy_kws_file
readelf -d build/armv7-uclibc/snowboy_kws_file
本次产物的关键特征是:
ELF 32-bit ARM EABI5
ARMv7-A
VFPv4
NEON
hard-float
动态依赖目标板的
libstdc++.so.6、libgcc_s.so.1和libc.so.0
x86 主机不能直接运行这个产物。目标板上如果缺少 C++ 运行库,需要随产品部署兼容版本的 libstdc++.so.6 和 libgcc_s.so.1,并正确设置 LD_LIBRARY_PATH。
四、原始 PortAudio Demo 遇到的编译和部署问题
这些问题来自早期直接打开麦克风的 C Demo。独立版适配器不依赖 PortAudio,但这些经验对排查旧 Demo 或其他板端录音程序仍然有用。
问题 1:PortAudio 没有启用 ALSA
现象:
Fail to open PortAudio stream, error message is Device unavailable.
Number of devices = 0
检查 portaudio/config.log,当时实际原因是:
cannot find -lasound
ALSA ........................ no
PortAudio 被交叉编译时没有找到目标侧 libasound,所以生成了一个没有 ALSA host API 的库。解决方法是把目标侧 ALSA 头文件和库路径传给 configure,并确认输出变成 ALSA yes:
./configure \
--without-jack \
--without-oss \
--disable-shared \
--enable-static \
--with-alsa \
--host=arm-rockchip830-linux-uclibcgnueabihf \
CC=arm-rockchip830-linux-uclibcgnueabihf-gcc \
CXX=arm-rockchip830-linux-uclibcgnueabihf-g++ \
CFLAGS="-mfloat-abi=hard -I$UCLIBC_PREFIX/include" \
LDFLAGS="-L$UCLIBC_PREFIX/lib"
问题 2:非 PIC 的静态 ALSA 库被拿去生成共享库
错误类似:
relocation R_ARM_MOVW_ABS_NC ... can not be used when making a shared object
recompile with -fPIC
原因是目标侧 libasound.a 没有用 PIC 编译,而 PortAudio 默认尝试构建共享库。项目实际只需要静态 libportaudio.a,因此关闭共享库构建:
--disable-shared --enable-static
这不是简单地给最终链接命令增加 -fPIC 就能解决的问题。静态依赖本身必须重新用 PIC 编译,或者避免把它放进共享对象。
问题 3:ALSA 配置文件路径不一致
板端实际配置在:
/usr/share/alsa/alsa.conf
但 PortAudio/ALSA 默认尝试访问:
/etc/alsa/alsa.conf
结果会出现:
Cannot access file /etc/alsa/alsa.conf
运行脚本中设置:
export ALSA_CONFIG_PATH=/usr/share/alsa/alsa.conf
如果系统允许,也可以让 /etc/alsa 指向 /usr/share/alsa。环境变量比把路径写死在程序中更容易适配不同 rootfs。
问题 4:ALSA 的 Unknown PCM 警告
枚举设备时可能看到:
Unknown PCM cards.pcm.front
Unknown PCM cards.pcm.rear
这些警告来自 ALSA 配置中不存在的可选 PCM 名称,不代表目标采集设备一定打不开。判断标准应该是:
PortAudio 是否识别到 ALSA host API
输入设备数量是否正确
实际采集流是否成功打开
/proc/asound中的采样率和格式是否正确
Demo 中通过 snd_lib_error_set_handler 静默 ALSA 内部警告,但仍保留真正的 PortAudio 打开失败信息。
问题 5:第二个进程打开设备失败
如果第一个 Demo 进程还在运行,第二个进程可能报:
Fail to open PortAudio stream, error message is Device unavailable.
排查命令:
ps | grep "[d]emo"
for d in /dev/snd/*; do fuser "$d" 2>/dev/null || true; done
cat /proc/asound/card0/pcm0c/sub0/status
当时设备状态显示已经被另一个 PID 占用。解决方法是先停止旧进程,再启动新实例。产品化时应让一个音频服务统一持有 ALSA 设备,唤醒检测只订阅 PCM。
问题 6:BusyBox tar 不支持 -z
某些板端 BusyBox 的 tar 不支持 -z,直接通过管道解压 gzip tar 会失败:
tar: invalid option -- z
可以改用未压缩 tar 流:
tar -C rv1106_dist -cf - . | \
ssh root@target-device 'tar -xf - -C /oem/snowboy_rv1106'
这属于部署工具能力差异,和 Snowboy 本身无关。
五、推荐的调试顺序
遇到板端唤醒失败时,按以下顺序排查:
用
file和readelf -A检查 ELF 架构、浮点 ABI 和指令集。检查
common.res和模型路径是否可读。检查 Snowboy 返回的采样率、声道数和位深。
先用 WAV 或裸 PCM 文件示例验证检测器,不要一开始就排查 ALSA。
如果文件检测成功,再检查音频服务实际输出是否为 16 kHz、mono、S16_LE。
如果使用旧 PortAudio Demo,再检查 ALSA host API、
ALSA_CONFIG_PATH和设备占用。最后调灵敏度、增益和前端开关;不要用参数问题掩盖 ABI 或音频格式问题。