1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)对正点原子Linux感兴趣的同学可以加群讨论:935446741
我们知道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控制,其他设备可以自行拓展。
语音识别技术产品,有讯飞,百度等厂家,我们可以购买或者免费试用他们的产品。可以直接到他们的官网上查看,有使用技术文档。下面我们以百度语音识别技术产品为例子。可以在浏览器输入搜索“百度语音识别”,就可以找到百度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包。
请认真阅读调用流程,了解操作过程,对下面理解编者编写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的示例吧。不能访问的话,编者也没办法的。
源码路径为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 [email protected]
* @net 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。
在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 [email protected]
* @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()<<"本地声卡设备:"<
36 }
37
38 /* 音频编码 */
39 codecsVar.append(QVariant(QString()));
40 for (auto &codecName: m_audioRecorder->supportedAudioCodecs()) {
41 codecsVar.append(QVariant(codecName));
42 //qDebug()<<"音频编码:"<
43 }
44
45 /* 容器/支持的格式 */
46 containersVar.append(QVariant(QString()));
47 for (auto &containerName: m_audioRecorder->supportedContainers()) {
48 containersVar.append(QVariant(containerName));
49 //qDebug()<<"支持的格式:"<
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()<<"采样率:"<
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()<
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()<<"通道数"<
266
267 /* 设置level的值 */
268 QVector<qreal> levels = getBufferLevels(buffer);
269 for (int i = 0; i < levels.count(); ++i) {
270 /* 打印音量等级 */
271 // qDebug()<<"音量等级"<
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后缀的文件。
项目路径为4/02_asr_demo/02_asr_demo/02_asr_demo.pro,先看项目界面。项目界面如下,界面简洁大气,界面中间用了一个立体的素材,点击后可以旋转,给人一种智能化的感觉,点击时还会有音效提示,文本提示“请点击,开始说话…”,点击后,提示“正在听您说话,请继续…”,录制8s左右的音频,等待返回识别结果即可。编写设计完成的效果不错!请自行查阅源码,掌握了本教程前面第七章的内容,就可以理解这个界面是如何设计的。
打开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文件夹为界面设计的源文件。
Ubuntu运行后界面如下,注意,Ubuntu需要联网!Ubuntu上理论上是能录制音频识别返回结果的,但是教程主要写正点原子I.MX6U开发板上的语音识别项目。限于编者手上没有可用电脑麦克风,估计读者也没有,电脑配置麦克风输入后可以自行测试。运行之后可以看到下面的界面。Windows不作讲解!请到下面小节使用正点原子I.MX6U 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。
“开灯”识别结果。
“关灯”识别结果。
本示例仅供学习参考使用,如需要用到开发上,请购买百度或其他开放平台的语音识别产品。