来源:
notes/documents/09_dg板卡X5_BPU语音转写_直调SDK实现与实测.mddg 板卡 X5:走 BPU 做语音转文字(已跑通,但为什么不划算)
一句话结论
在 dg(RDK X5)上可以绕过 ALSA 麦克风阵列,直接调用官方闭源 SDK 的 C 接口,把音频文件喂进 BPU 做转写——我把它跑通了。但实测下来没有实用优势:管道本身只能跑到约 2.6 倍实时(CPU 方案 5.5 倍),原因是 BPU 只承担了 encoder 一段,整条链路的耗时在 CPU 侧的串行前端处理和解码上。批量转写继续用 CPU;这条路的价值在于「实时拾音 + ASR 一体化」或「把 CPU 让出来」。
背景:之前的判断和这次的突破
之前的结论是「BPU 语音方案喂不了文件」,依据是官方 ROS 节点把输入硬编码成 8 麦阵列 ALSA 设备(micphone_chn=8),裸板报 Channels count (8) not available,而 SDK 是闭源库。
这次的突破点:官方 ROS 节点 D-Robotics/hobot_audio 是开源的,仓库里带着 SDK 头文件 include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h。头文件显示 SDK 提供的是「喂 PCM 帧」的接口,跟 ALSA 没有绑定——那个 8 麦限制只是 ROS 节点的实现方式,不是 SDK 的限制。
实现路径
- 从开源仓库拿到
hrsc_sdk.h,确认关键接口:HrscInit/HrscStart/HrscProcess/HrscRelease,回调HrscAsrCallback直接给最终文本 - 自己构造
HrscAudioBuffer,按帧喂 PCM,完全绕过 ALSA - 设
asr_output_mode = 2(0=不输出,1=唤醒后输出,2=全部输出)→ 不需要唤醒词,连续转写 - 用 g++ 链接板上的
/opt/tros/humble/lib/libhrsc.so,LD_LIBRARY_PATH指过去即可
模型确实跑在 BPU 上,日志证据:[BPU_PLAT] BPU Platform Version(1.3.6) soc info(x5)、[DNN] Runtime version = 1.24.5,加载 encoder.bin(175MB);ldd libhrsc.so 显示依赖 libdnn.so / libcnn_intf.so.1 / libhbmem.so.1。
踩过的三个坑
- BPU 内存整块独占:ION 的
cma_reserved堆只有 320MB,bpu_service占 312MB,175MB 的 encoder 必然分配失败(hbrtErrorBPUMemAllocFail)。跑之前必须systemctl stop bpu-service,跑完再启回来 → ASR 与视觉推理服务互斥 - 声道填充方式决定成败:6 个声道灌同样内容时,AEC/波束成形会把信号当回声消掉,只出碎片(4 段);改成只喂第 0 通道、其余静音后,输出变成与 CPU 基线基本一致的 11 段。2 通道和 8 通道配置直接无输出
- 必须接近实时喂数(或喂完后等管道排空):1x/2x 喂数出 11 段,4x 掉到 9 段,8x 剩 6 段,全速只出 1 段——但这不是数据丢失,
HrscProcess只是入队(30 秒音频仅耗时 0.03 秒),等足够久结果仍会全部返回
实测数据
管道真实耗时(30 秒音频,仅 ch0 填充,全速喂 + 排空)
| 指标 | 数值 |
|---|---|
HrscProcess 调用总耗时 |
0.03 秒(≈1000x,喂数不是瓶颈) |
| 首段结果返回 | +1.2 秒 |
| 末段结果返回 | +11.6 秒 → 管道本身约 2.6x 实时 |
| 端到端(含约 3.5 秒模型加载) | 16.8 秒 → 1.79x 实时 |
| CPU 时间 | user+sys 25.8 秒 / wall 16.8 秒 → 平均 1.54 核在忙 |
与 CPU 方案对照
| BPU(HRSC 直调) | CPU(sherpa-onnx paraformer-large int8,4 线程) | |
|---|---|---|
| 吞吐 | 约 2.6x 实时(含加载 1.8x) | 5.5x 实时(RTF 0.182) |
| 并行度 | 1.5 核 + BPU | 4 核吃满 |
| 30 秒音频 | 16.8 秒 | decode 10.9 秒(+加载 8.8 秒) |
| 全文 2h34m54s | 约 60 分钟(外推,且要停 bpu_service) | 约 29 分钟 |
| CPU 工作量 | 约 0.74 CPU-秒/音频秒 | 约 0.73 CPU-秒/音频秒 |
质量对照(同一段 30 秒)
- CPU:
电脑时代 p c 时代差不多是七五年七六年成立了两家公司一个叫苹果一个叫微软…还有一家公司叫 intel…开启了全人类的 p c 时代…在手机的计算平台 arm 高通…google 的安卓 - BPU:
能电脑时代pc时代 / 差不多多是七五年七六年 / 成立了两家公司 / 一个叫苹果 / 一个叫微软一 / 还有一家公司叫因子 / 开启了全人类的时代 / …在手机的计算屏 / um高通 / 包括google的安卓 - 差异:
intel→因子、计算平台→计算屏,少量丢字。整体可用,但弱于 CPU 方案
为什么 BPU 没有优势(定量)
- BPU 只接走 encoder 一段:AEC、AGC、波束成形、DOA、降噪、VAD、CTC 解码、FST 语言模型重打分、ITN 全在 CPU 侧(
libhrsc.so里的AecProcessBlock、CrgruVad*等) - 总 CPU 工作量几乎没变:BPU 管道 0.74 CPU-秒/音频秒 vs 纯 CPU 方案 0.73 —— offload 后 CPU 的活没减少,并行度却从 4 核降到 1.5 核,所以墙钟更慢
- 流式小 chunk 让 BPU 发挥不出来:
model.flag固定 chunk_size,batch=1、序列短,BPU 擅长的「大张量并行吞吐」用不上,大部分时间空闲 - CPU 方案本身太高效:离线整段推理 + 4 线程 + ONNX Runtime 的 NEON 优化,还省掉了整套交互式前端
- 结论:加速了链条上一小段,却牺牲了 4 核并行,净效果为负
什么情况下 BPU 才值得用
- 把 encoder+CTC 从交互式管道里单独抽出来(需要逆向闭源 SDK,或自己用量化工具链编译模型)
- 做多路并发批处理(当前 SDK 是单流)
- 目标不是吞吐而是释放 CPU / 低功耗——此时 BPU 有意义,CPU 可以去做别的事
复现步骤
# 1. 取头文件(从开源仓库)
curl -sL https://raw.githubusercontent.com/D-Robotics/hobot_audio/main/include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h -o /tmp/hrsc_sdk.h
# 2. 编译(源码见附录 A)
g++ -O2 -o /tmp/asr/hrsc_asr /tmp/asr/hrsc_asr.cpp -I/tmp \
-L/opt/tros/humble/lib -lhrsc -Wl,-rpath,/opt/tros/humble/lib -lpthread
# 3. 腾出 BPU 内存
systemctl stop bpu-service
# 4. 运行:<wav> <out.txt> <chn> <ref_ch> <asr_mode> <pace> <fill> <wait_s>
export LD_LIBRARY_PATH=/opt/tros/humble/lib
./hrsc_asr test30.wav out.txt 6 4 2 0 1 3
# 5. 恢复服务
systemctl start bpu-service
参数含义:chn=喂给 SDK 的通道数(6);ref_ch=参考通道(4);asr_mode=2 表示连续输出;pace=喂数限速(1.0 实时,0 全速);fill=1 表示只填第 0 通道、其余静音(关键);wait_s=喂完后等待异步结果的时间。
附录 A:完整代码(hrsc_asr.cpp)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <fstream>
#include <iostream>
#include <unistd.h>
#include <ctime>
#include <sys/resource.h>
#include "hrsc_sdk.h"
static std::vector<std::string> g_asr;
static struct timespec g_t0;
static double g_last_ms = 0;
static int g_n = 0;
static double now_ms() { struct timespec n; clock_gettime(CLOCK_MONOTONIC,&n); return (n.tv_sec-g_t0.tv_sec)*1000.0+(n.tv_nsec-g_t0.tv_nsec)/1e6; }
static void AsrCallback(const void *, const char *asr) {
if (asr && *asr) { double t=now_ms(); g_last_ms=t; g_n++; printf("\n[ASR +%.0fms] %s\n", t, asr); fflush(stdout); g_asr.push_back(asr); }
}
static void AsrDataCallback(const void *, const HrscCallbackData *d) {
if (d && d->result && *d->result) std::cout << "\n[DATA] " << d->result << std::endl;
}
static void EventCallback(const void *, HrscEventType e) { std::cout << "\n[EVENT] " << (int)e << std::endl; }
static void CmdCallback(const void *, const char *c) { if (c && *c) std::cout << "\n[CMD] " << c << std::endl; }
static void DoaCallback(const void *, int) {}
static void WkpCallback(const void *, const HrscCallbackData *, const int) {}
static void VoipCallback(const void *, const HrscCallbackData *) {}
static void ProcCallback(const void *, const HrscCallbackData *) {}
static void AuthCallback(const void *, const int code) { std::cout << "\n[AUTH] " << code << std::endl; }
static std::string ReadFile(const char *p) {
std::ifstream s(p, std::ios::in | std::ios::binary);
if (!s.is_open()) return "";
std::string d((std::istreambuf_iterator<char>(s)), std::istreambuf_iterator<char>());
return d;
}
int main(int argc, char **argv) {
if (argc < 3) { printf("usage: %s in16k_mono.wav out.txt [channels] [ref_ch] [asr_mode]\n", argv[0]); return 2; }
const char *wav_path = argv[1];
const char *out_path = argv[2];
int chn = argc > 3 ? atoi(argv[3]) : 6;
int ref_ch = argc > 4 ? atoi(argv[4]) : 6;
int asr_mode = argc > 5 ? atoi(argv[5]) : 2;
double pace = argc > 6 ? atof(argv[6]) : 0.0; // 1.0 = realtime, 0 = as fast as possible
int fill = argc > 7 ? atoi(argv[7]) : 0; // 0=all ch audio, 1=only ch0, 2=all but ref
printf("fill=%d\n", fill);
int wait_s = argc > 8 ? atoi(argv[8]) : 3;
FILE *f = fopen(wav_path, "rb");
if (!f) { printf("cannot open %s\n", wav_path); return 2; }
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 44, SEEK_SET);
if (sz <= 44) { printf("bad wav\n"); return 2; }
std::vector<short> pcm((sz - 44) / 2);
size_t got = fread(pcm.data(), 2, pcm.size(), f);
pcm.resize(got);
fclose(f);
int sr = 16000;
double dur = (double)pcm.size() / sr;
printf("input=%s frames=%zu dur=%.1fs chn=%d ref_ch=%d asr_mode=%d pace=%.2f\n", wav_path, pcm.size(), dur, chn, ref_ch, asr_mode, pace);
printf("sdk version: %s\n", HrscGetVersion());
HrscEffectConfig cfg; memset(&cfg, 0, sizeof(cfg));
static HrscAudioConfig in_cfg, out_cfg;
in_cfg.sample_rate = sr; in_cfg.audio_channels = chn; in_cfg.audio_format = kHrscAudioFormatPcm16Bit;
out_cfg.sample_rate = sr; out_cfg.audio_channels = 1; out_cfg.audio_format = kHrscAudioFormatPcm16Bit;
cfg.input_cfg = in_cfg;
cfg.output_cfg = out_cfg;
cfg.ref_ch_index = (unsigned char)ref_ch;
cfg.wakeup_prefix = 200; cfg.wakeup_suffix = 200;
cfg.asr_timeout = 5000; cfg.vad_timeout = 5000;
cfg.target_score = 0;
cfg.cfg_file_path = "/opt/tros/humble/lib/hobot_audio/hrsc";
cfg.support_command_word = 0;
cfg.is_use_linear_mic_flag = 0;
cfg.asr_output_mode = asr_mode;
cfg.asr_output_channel = 0;
std::string wake = ReadFile("/opt/tros/humble/lib/hobot_audio/hrsc/cmd_word.json");
cfg.custom_wakeup_word = wake.empty() ? nullptr : wake.c_str();
cfg.priv = &cfg;
cfg.HrscAsrCallback = AsrCallback;
cfg.HrscAsrDataCallback = AsrDataCallback;
cfg.HrscEventCallback = EventCallback;
cfg.HrscCmdCallback = CmdCallback;
cfg.HrscDoaCallbadk = DoaCallback;
cfg.HrscWakeupDataCallback = WkpCallback;
cfg.HrscVoipDataCallback = VoipCallback;
cfg.HrscProcessedDataCallback = ProcCallback;
cfg.HrscAuthCallback = AuthCallback;
void *handle = HrscInit(&cfg);
if (!handle) { printf("HrscInit FAILED\n"); return 3; }
if (HRSC_CODE_SUCCESS != HrscStart(handle)) { printf("HrscStart FAILED\n"); return 3; }
printf("hrsc started ok\n"); fflush(stdout);
clock_gettime(CLOCK_MONOTONIC, &g_t0);
struct timespec t_start = g_t0;
const int FRAME = 512;
double feed_secs = 0;
std::vector<short> frame(FRAME * chn);
size_t pos = 0; long long t_ms = 0;
while (pos < pcm.size()) {
size_t n = pcm.size() - pos; if (n > (size_t)FRAME) n = FRAME;
for (size_t i = 0; i < n; i++) {
for (int c = 0; c < chn; c++) {
bool use = (fill == 0) ? true : (fill == 1 ? (c == 0) : (c != ref_ch));
frame[i * chn + c] = use ? pcm[pos + i] : 0;
}
}
if (n < (size_t)FRAME) memset(&frame[n * chn], 0, (FRAME - n) * chn * sizeof(short));
HrscAudioBuffer buf; buf.audio_data = frame.data(); buf.size = FRAME * chn * sizeof(short);
buf.start = t_ms; buf.end = t_ms + 32;
{ struct timespec a,b; clock_gettime(CLOCK_MONOTONIC,&a); HrscProcess(handle, &buf); clock_gettime(CLOCK_MONOTONIC,&b); feed_secs += (b.tv_sec-a.tv_sec)+(b.tv_nsec-a.tv_nsec)/1e9; }
pos += n; t_ms += (FRAME * 1000) / sr;
if (pace > 0) {
long want_us = (long)((double)t_ms * 1000.0 / pace);
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now);
long now_us = (now.tv_sec - t_start.tv_sec) * 1000000L + (now.tv_nsec - t_start.tv_nsec) / 1000L;
if (want_us > now_us) usleep(want_us - now_us);
}
}
printf("\nfeed done, %.1fs audio, time_in_HrscProcess=%.2fs (=> %.2fx realtime), tail silence...\n", dur, feed_secs, dur/feed_secs); fflush(stdout);
for (int k = 0; k < 100; k++) {
memset(frame.data(), 0, frame.size() * sizeof(short));
HrscAudioBuffer buf; buf.audio_data = frame.data(); buf.size = frame.size() * sizeof(short);
buf.start = t_ms; buf.end = t_ms + 32;
HrscProcess(handle, &buf); t_ms += (FRAME * 1000) / sr;
usleep(1000);
}
printf("feed loop finished at +%.0fms, now draining async pipeline...\n", now_ms());
double drain_start = now_ms(), last_change = now_ms();
int last_n = g_n;
while (now_ms() - drain_start < 300000) {
usleep(200000);
if (g_n != last_n) { last_n = g_n; last_change = now_ms(); }
if (g_n > 0 && now_ms() - last_change > 5000) break;
}
{ struct rusage ru; getrusage(RUSAGE_SELF,&ru); double cpu=ru.ru_utime.tv_sec+ru.ru_utime.tv_usec/1e6+ru.ru_stime.tv_sec+ru.ru_stime.tv_usec/1e6; printf("\n[CPU] user+sys=%.2fs / wall=%.2fs => 平均 %.2f 核在忙\n", cpu, now_ms()/1000.0, cpu/(now_ms()/1000.0)); }
printf("\n=== 全部 %d 段结果到达,最后一段在 +%.0fms,端到端 %.2fs (音频 %.1fs => %.2fx 实时) ===\n", g_n, g_last_ms, now_ms()/1000.0, dur, dur/(now_ms()/1000.0));
std::ofstream o(out_path);
for (auto &s : g_asr) o << s << "\n";
o.close();
printf("\nsegments=%zu written=%s\n", g_asr.size(), out_path);
HrscStop(handle);
HrscRelease(&handle);
return 0;
}
附录 B:原始文件位置
- 本仓库:
notes/sources/dg_bpu_asr/hrsc_asr.cpp、notes/sources/dg_bpu_asr/measurements.md - dg(重启会丢):
/tmp/asr/hrsc_asr.cpp、/tmp/asr/test30.wav、/tmp/asr/bpu_*.txt - 头文件来源:https://github.com/D-Robotics/hobot_audio (
include/x5/horizon_speech_sdk/include/horizonspeechsdk/hrsc_sdk.h) - 相关笔记:方案总览
note_a4def66c886144e0;全文转写note_796c8125fe43479a