前言
随着学习的深入,感觉愈发缺乏满足感。刚好看到微信语音转文字的功能,经网上查询,发现可以使用 QT + 百度语音识别技术 实现这一功能。当然,由于使用的 QT 和 百度语音识别,那么看不到一些具体的底层实现,但操作起来相对比较简单。俗话说:“没吃过猪肉,还没见过猪跑?”,我打算先看看别人已有的技术,搬过来跑一下,然后再进行深入学习,同时也可以复习一下 QT 相关知识。文章如有写错或者代码可优化,欢迎大家指正!
QT 采集麦克风 pcm 音频裸数据
基础知识
PCM(Pulse Code Modulation,脉冲编码调制)⾳频数据是未经压缩的⾳频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准数字⾳频数据。
描述PCM数据的6个参数:
- Sample Rate : 采样频率。8kHz(电话)、44.1kHz(CD)、48kHz(DVD)。
- Sample Size : 量化位数。通常该值为16-bit。
- Number of Channels : 通道个数。常⻅的⾳频有⽴体声(stereo)和单声道(mono)两种类型,⽴体声包含左声道和右声道。另外还有环绕⽴体声等其它不太常⽤的类型。
- Sign : 表示样本数据是否是有符号位,⽐如⽤⼀字节表示的样本数据,有符号的话表示范围为-128 ~127,⽆符号是0 ~ 255。有符号位16bits数据取值范围为-32768~32767。
- Byte Ordering : 字节序。字节序是little-endian还是big-endian。通常均为little-endian。
- Integer Or Floating Point : 整形或浮点型。⼤多数格式的PCM样本数据使⽤整形表示,⽽在⼀些对精度要求⾼的应⽤⽅⾯,使⽤浮点类型表示PCM样本数据(浮点数 float值域为 [-1.0, 1.0])。
环境配置
第一步: 新建一个QWidget项目
第二步: 项目名与存放路径自选(然后一直下一步)
第三步: 在.pro文件中添加模块
QT += multimedia
第四步: 新建一个C++ Class(因为采集麦克风只是一个小功能,我们还有其他的功能),名字可自取。这里类名我起的是 AudioCapture
代码
audiocapture.h
#ifndef AUDIOCAPTURE_H
#define AUDIOCAPTURE_H
#include <QObject>
#include <QAudioInput>
#include <QFile>
#include <QMessageBox>
class AudioCapture : public QObject
{
Q_OBJECT
public:
explicit AudioCapture(QObject *parent = nullptr);
void startCapture(QString filename); //开始录音,文件名由调用者传入
void stopCapture(); //结束录音
~AudioCapture(); //析构函数,释放相关资源
signals:
private:
QAudioInput *pAudioInput; //录音对象
QFile *pFile; //存取文件
};
#endif // AUDIOCAPTURE_H
audiocapture.cpp
#include "audiocapture.h"
AudioCapture::AudioCapture(QObject *parent) : QObject(parent)
{
//初始化
pAudioInput = nullptr;
pFile = nullptr;
}
//开始录音
void AudioCapture::startCapture(QString filename)
{
//打开默认的音频输入设备
QAudioDeviceInfo audioDeviceInfo = QAudioDeviceInfo::defaultInputDevice();
//判断本地是否有录音设备
if(audioDeviceInfo.isNull() == false)
{
/* 创建文件并打开 */
pFile = new QFile;
pFile->setFileName(filename);
pFile->open(QIODevice::WriteOnly | QIODevice::Truncate);
// 设置音频文件格式
QAudioFormat format;
// 设置采样频率,常见的有16000、44100、48000
format.setSampleRate(16000);
// 设置通道数,单声道、双声道、5.1声道
format.setChannelCount(1);
// 设置每次采样得到的样本数据位值,8位、16位
format.setSampleSize(16);
// 设置编码方法
format.setCodec("audio/pcm");
// 判断当前设备设置是否支持该音频格式
if(audioDeviceInfo.isFormatSupported(format) == NULL)
{
format = audioDeviceInfo.nearestFormat(format);
}
// 创建录音对象
pAudioInput = new QAudioInput(format, this);
// 开始录音
pAudioInput->start(pFile);
}
else
{
// 没有录音设备
QMessageBox::information(NULL, tr("Record"), tr("Current No Record Device"));
}
}
void AudioCapture::stopCapture()
{
if(pAudioInput != NULL)
{
// 停止录音
pAudioInput->stop();
}
if(pFile != NULL)
{
// 关闭文件
pFile->close();
delete pFile;
pFile = nullptr;
}
}
AudioCapture::~AudioCapture()
{
//释放资源
if(pAudioInput != nullptr)
{
delete pAudioInput;
pAudioInput = nullptr;
}
if(pFile != nullptr)
{
delete pFile;
pFile = nullptr;
}
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "audiocapture.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_startPtn_clicked(); // 点击Start按钮后触发的槽函数
void on_stopPtn_clicked(); // 点击Stop按钮后触发的槽函数
private:
Ui::Widget *ui; //操作界面上的相关控件
AudioCapture myAudioCapture; //录音功能封装对象
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
ui->startPtn->setEnabled(true); //Start按钮初始化可用
ui->stopPtn->setEnabled(false); //Stop按钮初始化不可用
}
Widget::~Widget()
{
delete ui;
}
//点击Start按钮后触发的槽函数
void Widget::on_startPtn_clicked()
{
QString filepath = ui->filepath->text(); //获取用户输入地址
/* 判断用户是否输入地址 */
if(filepath == "")
{
QMessageBox::information(NULL, "information", "Please input the filepath to save!");
return;
}
/* 点击Start后禁用Start,开放Stop按钮 */
ui->startPtn->setEnabled(false);
ui->stopPtn->setEnabled(true);
myAudioCapture.startCapture(filepath); //开始录音
}
//点击Stop按钮后触发的槽函数
void Widget::on_stopPtn_clicked()
{
/* 点击Stop后禁用Stop,开放Start按钮 */
ui->startPtn->setEnabled(true);
ui->stopPtn->setEnabled(false);
myAudioCapture.stopCapture(); //结束录音
}
运行时界面UI(仅供测试,不够美观)
播放 pcm 数据
由于vlc播放器无法直接播放pcm音频裸数据,这里使用ffplay来播放(也可使用代码播放)
ffplay -f s16le -ar 16000 -ac 1 -i D:\\1.pcm
这里的参数设置需与代码中的设置一样,否则音效不对。
使用 QAudioOutput 来播放 pcm 音频数据
主要代码
//设置音频输出格式
QAudioFormat fmt;
//设置采样率
fmt.setSampleRate(44100);
//设置采样位数
fmt.setSampleSize(16);
//设置声道数
fmt.setChannelCount(1);
//设置解码方式
fmt.setCodec("audio/pcm");
// 设定字节序,以小端模式播放音频文件
fmt.setByteOrder(QAudioFormat::LittleEndian);
// 设定采样类型。根据采样位数来设定。
fmt.setSampleType(QAudioFormat::UnSignedInt);
// 创建QAudioOutput对象并初始化
QAudioOutput *out = new QAudioOutput(fmt);
// 调用start函数后,返回QIODevice对象的地址
QIODevice *io = out->start();
//获取设备播放一个周期所需要的字节数
int size = out->periodSize();
//创建缓冲区
char *buf = new char[size];
//以二进制只读方式打开pcm文件
FILE *fp = fopen("d:/1.pcm", "rb");
//判断是否读到末尾
while(!feof(fp))
{
//判断空闲空间是否小于一个周期的大小,如果是则说明CPU处理速度太快,得等一等。
if(out->bytesFree() < size)
{
QThread::msleep(1);
continue;
}
int len = fread(buf, 1, size, fp);
//判断是否成功读入
if(len <= 0)
{
break;
}
//这里相当于写入到电脑声卡的缓冲区,接下来的工作由声卡完成,与我们无关
io->write(buf, len);
}
fclose(fp); //关闭文件
//资源释放
if(NULL != buf)
{
delete buf;
buf = NULL;
}
if(NULL != out)
{
delete out;
out = NULL;
}
语音识别
百度智能云网址: https://cloud.baidu.com/product/speech.html?track=cf3e1b9d08c41e54e7f0ace5828291cce549454e8c470208
第一步: 点击右上角控制台,并完成登录
第二步: 点击右上角三条杠,然后选中语音技术
第三步: 概览中点击免费尝鲜
第四步: 选中短语音识别-普通话,然后左下角点击0元领取(这里我已经领过了,所以没有这个选项了)
第五步: 点击应用列表,然后创建应用,应用名称随意,应用归属选择个人即可,然后添加一些描述,创建即可(这里我昨天实验时创建过一个了)
第六步: 复制 APIKey 和 SecretKey
整个语音识别的逻辑分析
第一步: QT中使用QAudioInput进行麦克风采集pcm音频数据
第二步: 通过http的post方式将音频数据提交给百度后台进行语音识别
第三步: 百度返回识别后的数据,将数据显示到文本框中。
语音识别
网络请求主要代码
bool httppost::postMsg(QString url, QMap<QString, QString> headerdata, QByteArray requestData, QByteArray &replyData)
{
//发送请求的对象
QNetworkAccessManager manager;
//请求对象
QNetworkRequest request;
request.setUrl(url);
//设置请求参数
QMapIterator<QString, QString> it(headerdata);
while(it.hasNext())
{
it.next();
request.setRawHeader(it.key().toLatin1(),it.value().toLatin1());
}
QNetworkReply *reply = manager.post(request, requestData);
QEventLoop loop;
//一旦服务器返回,reply会发出信号
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
//死循环,reply发出信号,结束循环
//判断是否响应成功
if(reply != nullptr && reply->error() == QNetworkReply::NoError)
{
replyData = reply->readAll();
qDebug() << replyData;
return true;
}
else
{
qDebug() << "请求失败";
return false;
}
}
百度的接口相关设置
//获取access_token相关
const QString baiduTokenUrl = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%1&client_secret=%2&";
const QString client_id = 刚才复制的APIKey;
const QString client_secret = 刚才复制的SecretKey;
//普通话测试
const QString baiduSpeechurl = "http://vop.baidu.com/server_api?dev_pid=1537&cuid=%1&token=%2";
语音识别主要代码
QString speechrecognition::speechIdentify(QString filename)
{
//获取token
QString tokenUrl = QString(baiduTokenUrl).arg(client_id).arg(client_secret);
QMap<QString, QString> headers;
headers.insert(QString("Content-Type"), QString("audio/pcm;rate=16000"));
QByteArray requestdata; //发送的内容
QByteArray replydata; //服务器返回的内容
httppost httputil; //封装的网络请求类
bool success = httputil.postMsg(tokenUrl, headers, requestdata, replydata);
//判断是否请求成功
if(success)
{
QString key = "access_token";
//获取到access_token(通过json数据格式解析)
accessToken = getJsonvalue(replydata, key);
qDebug() << "----------------" << endl;
qDebug() << accessToken << endl;
}
else return "";
//语言识别
QString baiduSpeech = QString(baiduSpeechurl.arg("LAPTOP-71LN9B3Q").arg(accessToken));
//把文件转化为QByteArray
QFile file;
file.setFileName(filename);
file.open(QIODevice::ReadOnly);
requestdata = file.readAll();
file.close();
replydata.clear();
//再次发起http请求
bool result = httputil.postMsg(baiduSpeech, headers,requestdata,replydata);
//判断是否请求成功
if(result == true)
{
QString key = "result";
QString text = getJsonvalue(replydata,key); //获取识别后的文字
return text;
}
else
{
QMessageBox::warning(NULL, "识别提示", "识别失败");
return "";
}
}
QString speechrecognition::getJsonvalue(QByteArray ba, QString key)
{
QJsonParseError parseError;
QJsonDocument jsonDocument = QJsonDocument::fromJson(ba, &parseError);
if(parseError.error == QJsonParseError::NoError)
{
if(jsonDocument.isObject())
{
//jsonDocument转化成json对象
QJsonObject jsonObj = jsonDocument.object();
//判断是否包含key
if(jsonObj.contains(key))
{
QJsonValue jsonVal = jsonObj.value(key);
if(jsonVal.isString()) //字符串
{
return jsonVal.toString();
}
else if(jsonVal.isArray()) //数组
{
QJsonArray arr = jsonVal.toArray();
QJsonValue jv = arr.at(0);
return jv.toString();
}
}
}
}
return "";
}
效果展示
当点击start按钮后,语音描述"很高兴和大家一起学习音视频"
点击stop后,文本框内显示文字"很高兴和大家一起学习音视频"
过程中的一些坑(个人遇到的主要的坑)
- 音频采样参数必须一致,否则百度只会识别出嗯嗯嗯等一系列奇怪的词语
- 注意采样频率目前百度语音识别支持16000,使用其他的如44100/48000等会报错
- 注意.pro文件中要添加network模块,否则根本发送不出去,也就是说百度根本收不到,奇怪的是QT没给我直接报错,虽然没有提示,但也还是一点一点写了,结果发现根本没发送出去。