基于AM67A的嵌入式语音识别开发(四)--QT实现

最近在研究基于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.hlibvosk.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设计界面拖动,具体布局如下:
QT界面布局

控件name
窗口MainWindow400471
标题labelTitle20050
文本框textEdit360160
按钮1btnStart11040
按钮2btnStop11040
按钮3btnLoad11040

对前端有要求的也可以增加样式设计,现在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";
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值