最近在研究基于Python的语音模型在嵌入式板端的推理实现时,发现这类板卡对图像识别的支持更为成熟。市场上主打的AI功能大多围绕图像处理展开,而语音识别在使用深度学习加速库时效果并不理想。通过与TI销售沟通了解到,即便是车载系统的客户,也没有为语音唤醒功能专门配置硬件加速器,直接用ARM处理器运行即可。基于这些发现,我决定将重点转向实际运行方案的实现上。
思路分析
在嵌入式系统应用中,C语言是首选开发语言,并且硬件平台对C的支持更优。因此,我们需要先构建一个基于C的语音识别模型,然后通过QT设计用户界面,最终在开发板上运行程序进行功能测试。
1.Vosk API
我们选择了开源嵌入式语音模型Vosk,其特点包括:
- 提供C语言API接口,适配嵌入式系统和移动设备
- 支持轻量化语音识别模型部署
- 典型应用场景:小型设备、低功耗环境及实时语音识别需求
- 核心优势:多语言支持与优异的性能表现
1) 安装与准备
a. 下载Vosk C库 预编译的.so或者.dll,也可以源码编译
b. 下载Vosk语音识别模型 如 vosk-model-small-cn-0.22 中文模型
c. 在QT工程中,要把vosk_api.h 和 libvosk.so 加到工程里。
2) 文件目录
VoskQtDemo/
|—— data/
| |—— test.wav
|—— models/
| |—— vosk-model-small-cn-0.22/
|—— vosk/
| |—— include/
| | |—— vosk_api.h
| |—— lib/
| | |—— libvosk.dll // win64
| | |—— libvosk.lib // win64
| | |—— libvosk.so // x86 aarch64
|—— main.cpp
|—— mainwindow.h
|—— mainwindow.cpp
|—— VoskQtDemo.pro
2.QT项目配置
1) QT .pro文件
配置库文件路径
VOSK_DIR = $$PWD/vosk
LIBS += -L$$VOSK_DIR/lib -lvosk //Vosk库路径
# 包含路径
INCLUDEPATH += $$VOSK_DIR/include
2) 主要实现
加载模型:
// 固定音频路径
QString fileName = "../VoskQtDemo/data/test.wav";
// 加载模型(路径要改成你实际的模型目录)
qDebug() << "Current working directory:" << QDir::currentPath();
VoskModel *model = vosk_model_new("../VoskQtDemo/models/vosk-model-small-cn-0.22");
if (!model) {
QMessageBox::critical(this, "错误", "无法加载模型!");
return;
}
// 创建识别器,采样率 16000
VoskRecognizer *rec = vosk_recognizer_new(model, 16000.0);
// 打开 wav 文件
std::ifstream wav(fileName.toStdString(), std::ios::binary);
if (!wav.is_open()) {
QMessageBox::critical(this, "错误", "无法打开音频文件!");
vosk_model_free(model);
return;
}
// 跳过 WAV 文件头 (44 字节)
wav.seekg(44);
std::vector<char> buffer(4000);
QString resultText;
while (wav.read(buffer.data(), buffer.size()) || wav.gcount() > 0) {
int len = wav.gcount();
if (vosk_recognizer_accept_waveform(rec, buffer.data(), len)) {
std::string json = vosk_recognizer_result(rec);
resultText += parseVoskResult(json) + "\n";
}
}
// Final result
std::string finalJson = vosk_recognizer_final_result(rec);
resultText += parseVoskResult(finalJson);
这里的parseVoskResult函数是多做了一步提取,正常输出的格式是json,就像下面一样
{文本:你好世界}
很显然我们只需要”你好世界“,所以就去掉外面多余的内容
// 辅助函数:解析 Vosk JSON,提取 "text"
static QString parseVoskResult(const std::string &jsonStr)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(
QByteArray::fromStdString(jsonStr), &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject obj = doc.object();
return obj.value("text").toString();
}
return QString();
}
3) QT界面
可以mainwindow里写,也可以直接进入ui设计界面拖动,具体布局如下:

| 控件 | name | 宽 | 高 |
|---|---|---|---|
| 窗口 | MainWindow | 400 | 471 |
| 标题 | labelTitle | 200 | 50 |
| 文本框 | textEdit | 360 | 160 |
| 按钮1 | btnStart | 110 | 40 |
| 按钮2 | btnStop | 110 | 40 |
| 按钮3 | btnLoad | 110 | 40 |
对前端有要求的也可以增加样式设计,现在QT可以直接加载写好的样式表
// 加载样式表
QFile file("../VoskQtDemo/style/style.qss"); // 如果放在资源文件里
if (!file.open(QFile::ReadOnly)) {
qDebug() << "无法加载样式文件";
} else {
QString style = QLatin1String(file.readAll());
this->setStyleSheet(style);
}
样式表示例:
QWidget#centralwidget {
background-color: rgba(245, 245, 245, 200);
border-radius: 10px;
}
QLabel#labelTitle {
font-size: 22px;
font-weight: bold;
color: #1a73e8;
qproperty-alignment: 'AlignCenter';
margin-bottom: 15px;
}
QPushButton {
background-color: rgba(26, 115, 232, 220);
border-radius: 8px;
padding: 6px 14px;
color: white;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgba(22, 105, 193, 230);
}
QPushButton:pressed {
background-color: rgba(15, 76, 138, 240);
}
QTextEdit {
background-color: rgba(255, 255, 255, 180);
border: 1px solid #cccccc;
border-radius: 8px;
padding: 8px;
font-size: 16px;
color: #1a1a1a;
}
QScrollBar:vertical {
background: rgba(245,245,245,180);
width: 8px;
margin: 0px;
}
QScrollBar::handle:vertical {
background: rgba(160,160,160,200);
border-radius: 4px;
}
QScrollBar::handle:vertical:hover {
background: rgba(128,128,128,220);
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
background: none;
}
写好后样式如图:

4) 加载分析音频文件实例
void MainWindow::on_btnLoad_clicked()
{
QString fileName = "../VoskQtDemo/data/test.wav"; // 固定路径
if(!QFile::exists(fileName)) {
qDebug() << "音频文件不存在:" << fileName;
return;
}
// 加载模型
VoskModel *model = vosk_model_new("../VoskQtDemo/models/vosk-model-small-cn-0.22");
if(!model) {
qDebug() << "模型加载失败";
return;
}
VoskRecognizer *rec = vosk_recognizer_new(model, 16000.0);
std::ifstream wav(fileName.toStdString(), std::ios::binary);
if(!wav.is_open()) {
qDebug() << "无法打开音频文件";
vosk_model_free(model);
return;
}
wav.seekg(44);
std::vector<char> buffer(4000);
ui->textEdit->clear();
while(wav.read(buffer.data(), buffer.size()) || wav.gcount() > 0) {
int len = wav.gcount();
if(vosk_recognizer_accept_waveform(rec, buffer.data(), len)) {
std::string json = vosk_recognizer_result(rec);
QString text = parseVoskResult(json);
if(!text.isEmpty()){
ui->textEdit->moveCursor(QTextCursor::End);
ui->textEdit->insertPlainText(text + "\n");
ui->textEdit->verticalScrollBar()->setValue(ui->textEdit->verticalScrollBar()->maximum());
qApp->processEvents(); // 保证界面刷新
}
}
}
std::string finalJson = vosk_recognizer_final_result(rec);
QString finalText = parseVoskResult(finalJson);
if(!finalText.isEmpty()){
ui->textEdit->moveCursor(QTextCursor::End);
ui->textEdit->insertPlainText(finalText + "\n");
}
vosk_recognizer_free(rec);
vosk_model_free(model);
}
Vosk的识别率很高,基本都能完全正确

3.功能拓展
目前USB麦克风尚未配置完成,暂时使用电脑音频接口进行测试。功能流程如下:点击"开始录音"按钮启动录音,点击"录音结束"按钮保存录音文件,随后系统执行静态语音文件分析功能(上面写的)。
void MainWindow::on_btnStart_clicked()
{
// 音频格式
QAudioFormat format;
format.setSampleRate(16000); // 采样率
format.setChannelCount(1); // 单声道
format.setSampleSize(16); // 16-bit
format.setCodec("audio/pcm"); // PCM 编码
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info = QAudioDeviceInfo::defaultInputDevice();
if (!info.isFormatSupported(format)) {
qWarning() << "Default format not supported, trying nearest...";
format = info.nearestFormat(format);
}
// 输出文件
outputFile = new QFile("../VoskQtDemo/data/test.wav");
if (!outputFile->open(QIODevice::WriteOnly)) {
qWarning() << "无法打开文件 test.wav";
delete outputFile;
outputFile = nullptr;
return;
}
// 写 WAV 文件头 (先写一个空头,录音结束后补全)
QByteArray header(44, 0);
outputFile->write(header);
// 启动录音
audioInput = new QAudioInput(format, this);
audioInput->start(outputFile);
qDebug() << "录音开始...";
}
void MainWindow::on_btnStop_clicked()
{
if (audioInput && outputFile) {
audioInput->stop();
outputFile->close();
// 更新 WAV 文件头
QFile file("../VoskQtDemo/data/test.wav");
if (file.open(QIODevice::ReadWrite)) {
qint64 dataSize = file.size() - 44;
// RIFF 头
file.seek(0);
file.write("RIFF", 4);
quint32 fileSize = dataSize + 36;
file.write(reinterpret_cast<const char*>(&fileSize), 4);
file.write("WAVE", 4);
// fmt 块
file.write("fmt ", 4);
quint32 subchunk1Size = 16;
quint16 audioFormat = 1;
quint16 numChannels = 1;
quint32 sampleRate = 16000;
quint16 bitsPerSample = 16;
quint32 byteRate = sampleRate * numChannels * bitsPerSample / 8;
quint16 blockAlign = numChannels * bitsPerSample / 8;
file.write(reinterpret_cast<const char*>(&subchunk1Size), 4);
file.write(reinterpret_cast<const char*>(&audioFormat), 2);
file.write(reinterpret_cast<const char*>(&numChannels), 2);
file.write(reinterpret_cast<const char*>(&sampleRate), 4);
file.write(reinterpret_cast<const char*>(&byteRate), 4);
file.write(reinterpret_cast<const char*>(&blockAlign), 2);
file.write(reinterpret_cast<const char*>(&bitsPerSample), 2);
// data 块
file.write("data", 4);
file.write(reinterpret_cast<const char*>(&dataSize), 4);
file.close();
}
delete audioInput;
audioInput = nullptr;
delete outputFile;
outputFile = nullptr;
qDebug() << "录音结束,保存为 test.wav";
}
}
5364

被折叠的 条评论
为什么被折叠?



