正点原子 发表于 2021-7-20 12:40:19

《I.MX6U嵌入式Qt开发指南》第二十五章 语音识别项目

本帖最后由 正点原子 于 2021-8-11 12:23 编辑

1)实验平台:正点原子i.MX6ULL Linux阿尔法开发板
2)章节摘自【正点原子】《I.MX6U嵌入式Qt开发指南》
3)购买链接:https://item.taobao.com/item.htm?&id=603672744434
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开放平台。

      点击进去就可以看到他的技术文档链接位置。如下图。

      或者直接打开https://ai.baidu.com/ai-doc/SPEECH/Ek39uxgre就可以跳转到百度AI开放平台》帮助文档》语音技术页面。如下图。

      请仔细阅读百度语音技术的文档,里面写的非常详细,还有例子下载参考。
      编者阅读总结,想要使用百度语音识别接口,需要根据上面图中的新手指南注册百度帐号,领取免费额度及创建中文普通话应用(创建前先领取免费额度(180天免费额度,可调用约5万次左右,详细请看免费额度说明))。记住自己的密钥。请自行完成及创建百度帐号,按照百度帮助文档里的步骤,领取免费额度及创建中文普通话应用,获取密钥!程序里需要用到自己的密钥。编者提供的密钥是百度语音识别例程里的,如果开发次数超了可能就不能使用了。程序中只需要API Key与Secret Key。注意获取Access Token时有效期为30天,到期后需要在程序里重新获取新的token。

      更多参考请查看百度AI接入指南。
      注意,帮助文档里提及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
    * @author      Deng Zhimao
    * @email         <a href="mailto:1252699831@qq.com" target="_blank">1252699831@qq.com</a>
    * @net            <a href="www.openedv.com" target="_blank">www.openedv.com</a>
    * @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
17class Asr : public QWidget
18{
19      Q_OBJECT
20
21public:
22      Asr(QWidget *parent = nullptr);
23      ~Asr();
24
25      /* 请求网络 */
26      void requestNetwork(QString, QByteArray);
27
28      /* 获取识别结果 */
29      void getTheResult(QString fileName);
30
31private:
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
60private slots:
61
62      /* 准备读取响应返回来的数据 */
63      void readyReadData();
64
65      /* 响应完成处理 */
66      void replyFinished();
67
68signals:
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。
<font size="2">/******************************************************************
    Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
    * @projectName   audiorecorder
    * @brief         audiorecorder.cpp
    * @author      Deng Zhimao
    * @email         <a href="mailto:1252699831@qq.com" target="_blank" style="">1252699831@qq.com</a>
    * @net            <a href="www.openedv.com" target="_blank" style="">www.openedv.com</a>
    * @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
11template <class T>
12static QVector<qreal> getBufferLevels(const T *buffer, int frames, int channels);
13
14AudioRecorder::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
86AudioRecorder::~AudioRecorder()
87{
88}
89
90
91void 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.toInt());
105         settings.setBitRate(bitratesVar.toInt());
106         settings.setChannelCount(channelsVar.toInt());
107         settings.setQuality(QMultimedia::EncodingQuality(
108                                 qualityVar.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 = 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)
<span style="font-style: italic;"><span style="font-style: normal;">224               values /= 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</span><span style="font-style: normal;"> /= 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));
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 }</span></span></font>
      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项目下:
asr文件夹为语音识别的应用程序,主要用来与将录制的音频发送到百度云语音识别服务器上,然后返回识别结果。
aduiorecorder文件夹为录制wav音频的文件夹。主要是用来录制wav音频。
led文件夹为I.MX6U开发板控制LED的接口程序。
Headers文件夹为界面设计的头文件。
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用户快速体验手册,第3.15小节进行测试板子的录音功能。确保能正常录音,再交叉编译此Qt应用程序到开发板上运行。如何交叉编译Qt应用程序到开发板,请看【正点原子】I.MX6U 出厂系统Qt交叉编译环境搭建V1.x版本。
在正点原子I.MX6U开发板上运行此录音程序,需要先配置是麦克风(板子上的麦头)。
麦头录音,则在板子上运行开启麦头录音的脚本。
/home/root/shell/audio/mic_in_config.sh
      交叉编译到开发板上运行效果如下。下面的图都是开发板上的截图。
      程序初始化时。(注意开发板先插上网线联网!确保能上网!)

      点击中间的图标后,注意,请在点击1.5~2s后再说话,点击时有音效提醒,避免把音效录进去。整个录音过程是8s左右。

      识别返回结果的过程很快,识别率也挺高,如下图,编者说了一句“正点原子”,语音识别返回“正点原子”的结果。注意,识别是中文标准普通话。请尽量说一些日常话语,避免说生僻语句,特殊的方言等。识别常见问题请查看百度AI开发平台的帮助文档。

      再点击,再次进行语音识别,话语中,包含“开灯”,那么即可点亮板子上的LED。点亮后,再次进行语音识别,话语中包含“关灯”,即可熄灭板子上的LED。
      “开灯”识别结果。

      “关灯”识别结果。

      本示例仅供学习参考使用,如需要用到开发上,请购买百度或其他开放平台的语音识别产品。

页: [1]
查看完整版本: 《I.MX6U嵌入式Qt开发指南》第二十五章 语音识别项目