1)实验平台:正点原子阿尔法Linux开发板
2) 章节摘自【正点原子】《I.MX6U 嵌入式Qt开发指南》
3)购买链接:https://detail.tmall.com/item.htm?id=609033604451
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyz-i.mx6ull.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子阿尔法Linux交流群:1027879335
第二十五章 语音识别项目
我们知道AI智能音箱已经在我们生活中不少见,也许我们都玩过,智能化非常高,功能强大,与我们平常玩的那种蓝牙音箱,Wifi音箱有很大的区别,AI智能在哪里呢?语音识别技术和云端技术,主要由主控芯片,麦克风阵列,功率放大,codec,触控电路,LED阵列组成。 AI音箱对传统音箱主要有两大块的技术区别,一块是语音信号的前处理,包括回声消除、波速成型、音源定位、降噪、去混响、自动语音电平控制这块是偏硬件的控制。还有一块是智能语音交互,包括语音关键词搜索、本地语音识别、声纹识别、语音合成。 AI智能音箱的芯片方案商:联发科,全志科技,瑞芯微等等,语音识别都有现成的方案商。他们的麦克风阵列方案,有2麦,4麦,6麦,7 + 1麦等等。 写上面这些是让读者了解一下专业AI音箱方案与我们在正点原子Linux开发板想实现语音识别的差别在哪里。我们在正点原子Linux开发板上实现语音识别项目(功能),就不能与专业的AI音箱对比了。硬件资源有限,开发板只有一个麦头(咪头座),没有那些硬件控制消除回声,降噪等等。不过编者在上面调用百度语音API识别语音,识别率还是挺高的。 下面就与大家一起在正点原子Linux IMX6U开发板上实现语音识别功能吧!注意:正点原子MINI I.MX6U开发板没有音频芯片,不支持此实验,只有正点原子I.MX6U ALPHA开发板支持。 本章简介如下: 介绍百度语音技术账号申请,及简单介绍调用流程。 用Qt编写示例程序。流程如下,录制音频后,发送调用百度语音识别API接口,识别并返回结果。支持语音控制正点原子I.MX6U开发板上的LED控制,其他设备可以自行拓展。 25.1 语音识别产品申请帐号语音识别技术产品,有讯飞,百度等厂家,我们可以购买或者免费试用他们的产品。可以直接到他们的官网上查看,有使用技术文档。下面我们以百度语音识别技术产品为例子。可以在浏览器输入搜索“百度语音识别”,就可以找到百度AI开放平台。 点击进去就可以看到他的技术文档链接位置。如下图。 请仔细阅读百度语音技术的文档,里面写的非常详细,还有例子下载参考。 编者阅读总结,想要使用百度语音识别接口,需要根据上面图中的新手指南注册百度帐号,领取免费额度及创建中文普通话应用(创建前先领取免费额度(180天免费额度,可调用约5万次左右,详细请看免费额度说明))。记住自己的密钥。请自行完成及创建百度帐号,按照百度帮助文档里的步骤,领取免费额度及创建中文普通话应用,获取密钥!程序里需要用到自己的密钥。编者提供的密钥是百度语音识别例程里的,如果开发次数超了可能就不能使用了。程序中只需要API Key与Secret Key。注意获取Access Token时有效期为30天,到期后需要在程序里重新获取新的token。 注意,帮助文档里提及SDK包,有LinuxC++SDK包支持,但是目前仅支持 X64(x86-64) CPU架构的 Linux 操作系统。LinuxSDK 仅支持在线语音识别,固定长语音模式。简单的说就是还不支持ARM架构的SDK包。 25.2 百度语音识别流程及示例简介在百度AI帮助文档里可以看见如下重要信息。 请认真阅读调用流程,了解操作过程,对下面理解编者编写Qt调用百度语音API的例子会有一定的帮助。 总结:调用流程需要仔细阅读,百度提供了示例Demo代码,可以看到里面支持很多种编程语言编写的API请求相关示例demo代码。没有直接C++相关的代码。C语言是C++语言的子集,我们可以直接参考C语言编写的例子(请自行查阅及参考百度提供的C语言编写的API请求相关示例demo代码)来编写Qt调用语音识别API。(备注:其他语言编写的例子不在我们教程范围。)识别的音频格式支持如上,我们可以知道一些重要的信息是支持采样率16000、8000的固定值,16bit深的单声道,音频长度最长60秒。格式支持wav,恰好正点原子Linux I.MX6U开发板系统支持wav格式播放及录制(详细请看【正点原子】I.MX6U用户快速体验V1.x.pdf测试音频部分)。 备注:由于百度语音识别的API例子放在github(开源网站),国外网站的原因,可能打开失败,请多次尝试,如果一直无法访问,那么我们直接往下看使用编者编写Qt的示例吧。不能访问的话,编者也没办法的。 25.3 百度短语音识别API接口源码路径为4/02_asr_demo/asr/asr.h,内容如下。asr是语音识别功能demo,(asr译作自动语音识别技术即automatic speech recognition) - /******************************************************************
- Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
- * @projectName asr
- * @brief asr.h
- * [url=home.php?mod=space&uid=90321]@Author[/url] Deng Zhimao
- * [url=home.php?mod=space&uid=55957]@EMAIL[/url] 1252699831@qq.com
- * [url=home.php?mod=space&uid=28414]@net[/url] www.openedv.com
- * @date 2021-06-03
- *******************************************************************/
- 1 #ifndef ASR_H
- 2 #define ASR_H
- 3
- 4 #include <QWidget>
- 5
- 6 #include <QNetworkAccessManager>
- 7 #include <QNetworkReply>
- 8
- 9 #include <QJsonDocument>
- 10 #include <QJsonParseError>
- 11 #include <QJsonObject>
- 12 #include <QJsonArray>
- 13 #include <QHostInfo>
- 14
- 15 #include <QFile>
- 16
- 17 class Asr : public QWidget
- 18 {
- 19 Q_OBJECT
- 20
- 21 public:
- 22 Asr(QWidget *parent = nullptr);
- 23 ~Asr();
- 24
- 25 /* 请求网络 */
- 26 void requestNetwork(QString, QByteArray);
- 27
- 28 /* 获取识别结果 */
- 29 void getTheResult(QString fileName);
- 30
- 31 private:
- 32 /* 存储获取tokenUrl地址 */
- 33 QString tokenUrl;
- 34
- 35 /* 存储serverapi地址 */
- 36 QString serverApiUrl;
- 37
- 38 /* 最终需要访问token的地址 */
- 39 QString accessToken;
- 40
- 41 /* 获取token的接口*/
- 42 const QString token_org = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%1&client_secret=%2&";
- 43
- 44 /* 填写网页上申请的appkey 如 g_api_key="g8eBUMSokVB1BHGmgxxxxxx" */
- 45 const QString api_key = "kVcnfD9iW2XVZSMaLMrtLYIz";
- 46
- 47 /* 填写网页上申请的APP SECRET 如 $secretKey="94dc99566550d87f8fa8ece112xxxxx" */
- 48 const QString secret_key = "O9o1O213UgG5LFn0bDGNtoRN3VWl2du6";
- 49
- 50 /* 百度服务器API接口,发送语音可返回识别结果 */
- 51 const QString server_api = "http://vop.baidu.com/server_api?dev_pid=1537&cuid=%1&token=%2";
- 52
- 53 /* 网络管理 */
- 54 QNetworkAccessManager *networkAccessManager;
- 55
- 56 QString getJsonValue(QByteArray ba, QString key);
- 57
- 58 QFile file;
- 59
- 60 private slots:
- 61
- 62 /* 准备读取响应返回来的数据 */
- 63 void readyReadData();
- 64
- 65 /* 响应完成处理 */
- 66 void replyFinished();
- 67
- 68 signals:
- 69 void asrReadyData(QString);
- 70
- 71 };
- 72 #endif // ASR_H
复制代码 第45行,请填写读者自己在网页上申请的API Key。以防万一示例中的API Key过期不可用! 第47行,请填写读者在网页上申请的Secret Key。以防万一示例中的Secret Key过期不可用! 其他地址由来是见百度给出的Demo示例,及百度的帮助文档。这里就不详细说了。原理与上一章原子云API接口相似。不过百度语音识别需要通过自己的帐号,指定地址获取访问的Token源地址,然后将得到的Access Token地址与语音识别服务器地址拼接,发送语音到服务器,就可以返回识别的结果了。详细请参考源码4/02_asr_demo/asr/asr.cpp。 25.4 录制wav音频在12.5小节,已经介绍过开发板如何录制音频文件了,详细请看12.5小节,就不详细介绍了,注意需要修改的地方如下。因为百度语音识别支持采样率16000、8000的固定值,16bit深的单声道,音频长度最长60秒。格式支持wav,pcm等格式。我们需要修改录制音频格式为wav格式,通道为单声道,采样率为16000,源码如下,已用红色字体标出。录制的音频文件保存为本地16k.wav文件。 源码路径为4/02_asr_demo/audiorecorder/audiorecorder.cpp。
- /******************************************************************
- Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
- * @projectName audiorecorder
- * @brief audiorecorder.cpp
- * @author Deng Zhimao
- * @email 1252699831@qq.com
- * @net www.openedv.com
- * @date 2021-06-04
- *******************************************************************/
- 1 #include "audiorecorder.h"
- 2 #include <QDebug>
- 3 #include <QUrl>
- 4 #include <QDateTime>
- 5 #include <QDir>
- 6 #include <QCoreApplication>
- 7
- 8 static qreal getPeakValue(const QAudioFormat &format);
- 9 static QVector<qreal> getBufferLevels(const QAudioBuffer &buffer);
- 10
- 11 template <class T>
- 12 static QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels);
- 13
- 14 AudioRecorder::AudioRecorder(QWidget *parent)
- 15 {
- 16 Q_UNUSED(parent);
- 17
- 18 /* 录制音频的类 */
- 19 m_audioRecorder = new QAudioRecorder(this);
- 20
- 21 /* 用于探测缓冲区的数据 */
- 22 m_probe = new QAudioProbe(this);
- 23
- 24 /* 信号槽连接,更新录音level显示 */
- 25 connect(m_probe, &QAudioProbe::audioBufferProbed,
- 26 this, &AudioRecorder::processBuffer);
- 27
- 28 /* 设置探测的对象 */
- 29 m_probe->setSource(m_audioRecorder);
- 30
- 31 /* 扫描本地声卡设备 */
- 32 devicesVar.append(QVariant(QString()));
- 33 for (auto &device: m_audioRecorder->audioInputs()) {
- 34 devicesVar.append(QVariant(device));
- 35 //qDebug()<<"本地声卡设备:"<<device<<endl;
- 36 }
- 37
- 38 /* 音频编码 */
- 39 codecsVar.append(QVariant(QString()));
- 40 for (auto &codecName: m_audioRecorder->supportedAudioCodecs()) {
- 41 codecsVar.append(QVariant(codecName));
- 42 //qDebug()<<"音频编码:"<<codecName<<endl;
- 43 }
- 44
- 45 /* 容器/支持的格式 */
- 46 containersVar.append(QVariant(QString()));
- 47 for (auto &containerName: m_audioRecorder->supportedContainers()) {
- 48 containersVar.append(QVariant(containerName));
- 49 //qDebug()<<"支持的格式:"<<containerName<<endl;
- 50 }
- 51
- 52 /* 采样率 */
- 53 sampleRateVar.append(QVariant(0));
- 54 /* 百度语音识别只支持8000、 16000采样率 */
- 55 sampleRateVar.append(QVariant(8000));
- 56 sampleRateVar.append(QVariant(16000));
- 57 for (int sampleRate: m_audioRecorder->supportedAudioSampleRates()) {
- 58 sampleRateVar.append(QVariant(sampleRate));
- 59 //qDebug()<<"采样率:"<<sampleRate<<endl;
- 60 }
- 61
- 62
- 63 /* 通道 */
- 64 channelsVar.append(QVariant(-1));
- 65 channelsVar.append(QVariant(1));
- 66 channelsVar.append(QVariant(2));
- 67 channelsVar.append(QVariant(4));
- 68
- 69 /* 质量 */
- 70 qualityVar.append(QVariant(int(QMultimedia::LowQuality)));
- 71 qualityVar.append(QVariant(int(QMultimedia::NormalQuality)));
- 72 qualityVar.append(QVariant(int(QMultimedia::HighQuality)));
- 73
- 74 /* 比特率 */
- 75 bitratesVar.append(QVariant(0));
- 76 bitratesVar.append(QVariant(32000));
- 77 bitratesVar.append(QVariant(64000));
- 78 bitratesVar.append(QVariant(96000));
- 79 bitratesVar.append(QVariant(128000));
- 80
- 81 /* 录音类信号槽连接 */
- 82 connect(m_audioRecorder, &QAudioRecorder::durationChanged,
- 83 this, &AudioRecorder::updateProgress);
- 84 }
- 85
- 86 AudioRecorder::~AudioRecorder()
- 87 {
- 88 }
- 89
- 90
- 91 void AudioRecorder::startRecorder()
- 92 {
- 93 /* 备注:录音需要设置成16000 采样率和通道数为1,
- 94 * 保存为wav文件需要设置成audio/x-wav(container文件格式) */
- 95
- 96 /* 如果录音已经停止,则开始录音 */
- 97 if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
- 98 /* 设置默认的录音设备 */
- 99 m_audioRecorder->setAudioInput(devicesVar.at(0).toString());
- 100
- 101 /* 下面的是录音设置 */
- 102 QAudioEncoderSettings settings;
- 103 settings.setCodec(codecsVar.at(0).toString());
- 104 settings.setSampleRate(sampleRateVar[2].toInt());
- 105 settings.setBitRate(bitratesVar[0].toInt());
- 106 settings.setChannelCount(channelsVar[1].toInt());
- 107 settings.setQuality(QMultimedia::EncodingQuality(
- 108 qualityVar[0].toInt()));
- 109
- 110 /* 以恒定的质量录制,可选恒定的比特率 */
- 111 settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
- 112
- 113 /* I.MX6ULL第20个支持的格式为 audio/x-wav */
- 114 QString container = containersVar.at(20).toString();
- 115
- 116 /* 使用配置 */
- 117 m_audioRecorder->setEncodingSettings(settings,
- 118 QVideoEncoderSettings(),
- 119 container);
- 120 /* 录音保存为16k.wav文件 */
- 121 m_audioRecorder->setOutputLocation(QUrl::fromLocalFile(tr("./16k.wav")));
- 122
- 123 /* 开始录音 */
- 124 m_audioRecorder->record();
- 125 }
- 126 }
- 127
- 128 void AudioRecorder::stopRecorder()
- 129 {
- 130 /* 停止录音 */
- 131 m_audioRecorder->stop();
- 132 }
- 133
- 134
- 135 void AudioRecorder::updateProgress(qint64 duration)
- 136 {
- 137 Q_UNUSED(duration);
- 138
- 139 if (m_audioRecorder->error()
- 140 != QMediaRecorder::NoError)
- 141 return;
- 142
- 143 /* 打印录制时长 */
- 144 //qDebug()<<duration / 1000<<endl;
- 145 }
- 146
- 147
- 148 void AudioRecorder::clearAudioLevels()
- 149 {
- 150 //...
- 151 }
- 152
- 153 // This function returns the maximum possible sample value for a given audio format
- 154 qreal getPeakValue(const QAudioFormat& format)
- 155 {
- 156 // Note: Only the most common sample formats are supported
- 157 if (!format.isValid())
- 158 return qreal(0);
- 159
- 160 if (format.codec() != "audio/pcm")
- 161 return qreal(0);
- 162
- 163 switch (format.sampleType()) {
- 164 case QAudioFormat::Unknown:
- 165 break;
- 166 case QAudioFormat::Float:
- 167 if (format.sampleSize() != 32) // other sample formats are not supported
- 168 return qreal(0);
- 169 return qreal(1.00003);
- 170 case QAudioFormat::SignedInt:
- 171 if (format.sampleSize() == 32)
- 172 return qreal(INT_MAX);
- 173 if (format.sampleSize() == 16)
- 174 return qreal(SHRT_MAX);
- 175 if (format.sampleSize() == 8)
- 176 return qreal(CHAR_MAX);
- 177 break;
- 178 case QAudioFormat::UnSignedInt:
- 179 if (format.sampleSize() == 32)
- 180 return qreal(UINT_MAX);
- 181 if (format.sampleSize() == 16)
- 182 return qreal(USHRT_MAX);
- 183 if (format.sampleSize() == 8)
- 184 return qreal(UCHAR_MAX);
- 185 break;
- 186 }
- 187
- 188 return qreal(0);
- 189 }
- 190
- 191 // returns the audio level for each channel
- 192 QVector<qreal> getBufferLevels(const QAudioBuffer& buffer)
- 193 {
- 194 QVector<qreal> values;
- 195
- 196 if (!buffer.format().isValid() || buffer.format().byteOrder() != QAudioFormat::LittleEndian)
- 197 return values;
- 198
- 199 if (buffer.format().codec() != "audio/pcm")
- 200 return values;
- 201
- 202 int channelCount = buffer.format().channelCount();
- 203 values.fill(0, channelCount);
- 204 qreal peak_value = getPeakValue(buffer.format());
- 205 if (qFuzzyCompare(peak_value, qreal(0)))
- 206 return values;
- 207
- 208 switch (buffer.format().sampleType()) {
- 209 case QAudioFormat::Unknown:
- 210 case QAudioFormat::UnSignedInt:
- 211 if (buffer.format().sampleSize() == 32)
- 212 values = getBufferLevels(buffer.constData<quint32>(), buffer.frameCount(), channelCount);
- 213 if (buffer.format().sampleSize() == 16)
- 214 values = getBufferLevels(buffer.constData<quint16>(), buffer.frameCount(), channelCount);
- 215 if (buffer.format().sampleSize() == 8)
- 216 values = getBufferLevels(buffer.constData<quint8>(), buffer.frameCount(), channelCount);
- 217 for (int i = 0; i < values.size(); ++i)
- 218 values[i] = qAbs(values.at(i) - peak_value / 2) / (peak_value / 2);
- 219 break;
- 220 case QAudioFormat::Float:
- 221 if (buffer.format().sampleSize() == 32) {
- 222 values = getBufferLevels(buffer.constData<float>(), buffer.frameCount(), channelCount);
- 223 for (int i = 0; i < values.size(); ++i)
- 224 values[i] /= peak_value;
- 225 }
- 226 break;
- 227 case QAudioFormat::SignedInt:
- 228 if (buffer.format().sampleSize() == 32)
- 229 values = getBufferLevels(buffer.constData<qint32>(), buffer.frameCount(), channelCount);
- 230 if (buffer.format().sampleSize() == 16)
- 231 values = getBufferLevels(buffer.constData<qint16>(), buffer.frameCount(), channelCount);
- 232 if (buffer.format().sampleSize() == 8)
- 233 values = getBufferLevels(buffer.constData<qint8>(), buffer.frameCount(), channelCount);
- 234 for (int i = 0; i < values.size(); ++i)
- 235 values[i] /= peak_value;
- 236 break;
- 237 }
- 238
- 239 return values;
- 240 }
- 241
- 242 template <class T>
- 243 QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels)
- 244 {
- 245 QVector<qreal> max_values;
- 246 max_values.fill(0, channels);
- 247
- 248 for (int i = 0; i < frames; ++i) {
- 249 for (int j = 0; j < channels; ++j) {
- 250 qreal value = qAbs(qreal(buffer[i * channels + j]));
- 251 if (value > max_values.at(j))
- 252 max_values.replace(j, value);
- 253 }
- 254 }
- 255
- 256 return max_values;
- 257 }
- 258
- 259 void AudioRecorder::processBuffer(const QAudioBuffer& buffer)
- 260 {
- 261 /* 根据通道数目需要显示count个level */
- 262 int count = buffer.format().channelCount();
- 263 /* 打印通道数 */
- 264 Q_UNUSED(count);
- 265 // qDebug()<<"通道数"<<count<<endl;
- 266
- 267 /* 设置level的值 */
- 268 QVector<qreal> levels = getBufferLevels(buffer);
- 269 for (int i = 0; i < levels.count(); ++i) {
- 270 /* 打印音量等级 */
- 271 // qDebug()<<"音量等级"<<levels.at(i)<<endl;
- 272 }
- 273 }
复制代码4/02_asr_demo/audiorecorder/audiorecorder.cpp主要提供了一个startRecorder()和stopRecorder()的接口,录音保存的文件为可执行程序当前路径下的16k.wav文件。startRecorder()和stopRecorder()分别是开始录音和停止录音。 第54~56行,增加8000和16000的支持项。 第104行,设置为下标为2的项,也就是16000采样率。 第106行,设置通道数下标为1的项,也就是单通道。 第114行,设置文件容器/格式,为audio/x-wav格式(项的下标为20)。设置此格式会保存wav后缀的文件。 25.5 语音界面UI开发项目路径为4/02_asr_demo/02_asr_demo/02_asr_demo.pro,先看项目界面。项目界面如下,界面简洁大气,界面中间用了一个立体的素材,点击后可以旋转,给人一种智能化的感觉,点击时还会有音效提示,文本提示“请点击,开始说话…”,点击后,提示“正在听您说话,请继续…”,录制8s左右的音频,等待返回识别结果即可。编写设计完成的效果不错!请自行查阅源码,掌握了本教程前面第七章的内容,就可以理解这个界面是如何设计的。 25.6 语音识别项目综合测试打开4/02_asr_demo/02_asr_demo/02_asr_demo.pro项目,此项目为语音识别UI界面。 打开项目如下图。 项目文件夹下内容解释: 02_asr_demo项目下: l asr文件夹为语音识别的应用程序,主要用来与将录制的音频发送到百度云语音识别服务器上,然后返回识别结果。 l aduiorecorder文件夹为录制wav音频的文件夹。主要是用来录制wav音频。 l led文件夹为I.MX6U开发板控制LED的接口程序。 l Headers文件夹为界面设计的头文件。 l Sources文件夹为界面设计的源文件。 25.6.1 Ubuntu上运行Ubuntu运行后界面如下,注意,Ubuntu需要联网!Ubuntu上理论上是能录制音频识别返回结果的,但是教程主要写正点原子I.MX6U开发板上的语音识别项目。限于编者手上没有可用电脑麦克风,估计读者也没有,电脑配置麦克风输入后可以自行测试。运行之后可以看到下面的界面。Windows不作讲解!请到下面小节使用正点原子I.MX6U ALPHA开发板运行体验识别效果! 25.6.2 ALPHA开发板上运行本例适用于正点原子I.MX6U ALPHA开发板!请使用正点原子I.MX6U的出厂系统进行测试! 请使用正点原子的I.MX6U的出厂时的系统测试! 请使用正点原子的I.MX6U的出厂时的系统测试! 请使用正点原子的I.MX6U的出厂时的系统测试! 重要的事情是说三遍! 在正点原子I.MX6U开发板上运行此录音程序,需要先配置是麦克风(板子上的麦头)。 麦头录音,则在板子上运行开启麦头录音的脚本。 /home/root/shell/audio/mic_in_config.sh 交叉编译到开发板上运行效果如下。下面的图都是开发板上的截图。 程序初始化时。(注意开发板先插上网线联网!确保能上网!) 点击中间的图标后,注意,请在点击1.5~2s后再说话,点击时有音效提醒,避免把音效录进去。整个录音过程是8s左右。 识别返回结果的过程很快,识别率也挺高,如下图,编者说了一句“正点原子”,语音识别返回“正点原子”的结果。注意,识别是中文标准普通话。请尽量说一些日常话语,避免说生僻语句,特殊的方言等。识别常见问题请查看百度AI开发平台的帮助文档。 再点击,再次进行语音识别,话语中,包含“开灯”,那么即可点亮板子上的LED。点亮后,再次进行语音识别,话语中包含“关灯”,即可熄灭板子上的LED。 “开灯”识别结果。 “关灯”识别结果。 本示例仅供学习参考使用,如需要用到开发上,请购买百度或其他开放平台的语音识别产品。
|