返回文章
SnowboyKWSRV1106ARMuClibc

Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障

Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障 项目概述 这次把产品里的 Snowboy 语音唤醒部分抽成了独立项目: GitHub:<https://github.com/deng weihong/snowboy kws 当前提交:\ ec24110\ 目标平台:Rockchip

2026年9月24日阅读约 13 分钟0 条评论

Snowboy KWS 在 RV1106/uClibc 上的原理、独立开源与编译排障

项目概述

这次把产品里的 Snowboy 语音唤醒部分抽成了独立项目:

  • GitHub:https://github.com/deng-weihong/snowboy-kws

  • 当前提交: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 构造时加载两个文件:

  1. common.res:Snowboy 的公共资源文件,包含特征处理和检测网络运行所需的资源。

  2. .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 位 ARM

  • rockchip830:工具链面向 Rockchip 830 系列平台

  • linux-uclibc:目标 C 运行库是 uClibc

  • gnueabihf:使用 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 本身无关。

五、推荐的调试顺序

遇到板端唤醒失败时,按以下顺序排查:

  1. 用 file 和 readelf -A 检查 ELF 架构、浮点 ABI 和指令集。

  2. 检查 common.res 和模型路径是否可读。

  3. 检查 Snowboy 返回的采样率、声道数和位深。

  4. 先用 WAV 或裸 PCM 文件示例验证检测器,不要一开始就排查 ALSA。

  5. 如果文件检测成功,再检查音频服务实际输出是否为 16 kHz、mono、S16_LE。

  6. 如果使用旧 PortAudio Demo,再检查 ALSA host API、ALSA_CONFIG_PATH 和设备占用。

  7. 最后调灵敏度、增益和前端开关;不要用参数问题掩盖 ABI 或音频格式问题。

Discussion

评论

0 条已通过审核

留下评论

友善交流,请勿发布广告或无关内容。