【RealtekRTL8722DM物联网开发套件】+云语音识别
非常感谢 Realtek 与面包板平台提供的 RTL8722DM MINI 的测评机会。以下是我基于该套件实现的云语音识别开发流程以及最终效果。

项目源码:https://github.com/ha-zhuzhu/RTL8722DM-MINI-speech_recognizer

项目演示视频:

前言

Ameba RTL8722DM MINI 板载功能非常丰富,支持 Wifi,蓝牙,Codec,麦克风耳机孔,Micro SD卡接口等……并且还有丰富的外围接口可以连接各种传感器。因此我们能够通过这块套件实现绝大部分物联网场景下的应用,如云语音识别。

Realtek 官网上提供了 RTL8722DM MINI 详细资料,并给出了针对其不同功能的各种 ARDUINO 示例程序,数量多达七十多个,而且各种库写的非常简洁、源码注释清晰,因此开发起来非常舒适。

简单浏览,找到几个与本测评项目相关的库:

  • HttpClient:支持http协议一些比较基本的操作。
  • FatFs_SD:读写 Fat 文件格式的 SD 卡。
  • RecordWav:能直接录制 wav 格式音频到 SD 卡上,也可以播放。

因此,云语音识别的实现流程为:

  • 按键检测,利用 RecordWav 录制音频到 SD 卡。

  • 利用 FatFs_SD 读取刚录制的音频。

  • 利用 HttpClient 将音频 POST 到服务器,服务器调用语音识别 api 后将识别结果返回给 Ameba。最后返回流程1。

开发流程


Ameba


HttpClient

HttpClient 支持 HTTP 的各种请求,不过功能还不够完善。目前该库还不支持持久连接,如 post() 源码的主要调用流程为:

  • startRequest():


    • sendInitialHeaders():发送请求头信息,包括“Connection: close”
      1. /* HttpClient.cpp */
      2. // We don't support persistent connections, so tell the server to
      3. // close this connection after we're done
      4. sendHeader(HTTP_HEADER_CONNECTION, "close");
    • finishHeaders():发送空行结束 header


这样 POST 在发送请求头信息后就会结束连接,因此我们需要作一定的拓展。

POST 二进制流类型的文件对 POST 请求头和请求体的格式具有一定要求,我们只需要按照要求的格式和流程发送数据即可。由于我们一次流程只发送一个语音文件,并且其体积较大,因此请求体中只有头部及尾部的两个 boundary,以及中间的音频内容。因此我们可以提前设置好两个 boundary,以及 Content_Type, Content-Length 等内容以制作请求头,并在请求体中分三个部分发送即可:
  1. /* speech_recognizer.ino */
  2. char Content_Type[] = "multipart/form-data; boundary=----WebKitFormBoundarypNjgoVtFRlzPquKE";
  3. // 请求体头部及尾部数据
  4. char post_start[] = "------WebKitFormBoundarypNjgoVtFRlzPquKE\r\nContent-Disposition: form-data; name="file"; filename="ameba_recording"\r\nContent-Type: application/octet-stream\r\n\r\n";
  5. char post_end[] = "\r\n------WebKitFormBoundarypNjgoVtFRlzPquKE--\r\n";
  6. const int post_start_len = strlen(post_start);
  7. const int post_end_len = strlen(post_end);
在 setup() 中将它们写入我们在 HttpClient 中添加的成员变量中。在本项目中需要一边读取 sd 卡一边发送,因此不提前设置 post_content。
  1. /* speech_recognizer.ino */
  2. void setup()
  3. {
  4.     uint8_t *post_content = NULL;
  5.     http.mysetPostData(post_start, post_end, post_content, post_start_len, post_end_len, 0, Content_Type);
  6. }
  7. /* HttpClient.cpp */
  8. int HttpClient::mysetPostData(char *post_start_d, char *post_end_d, uint8_t *post_content_d, int post_start_len_d,
  9.                               int post_end_len_d, int post_content_len_d, char *content_type_d)
  10. {
  11.     post_start = post_start_d;
  12.     post_end = post_end_d;
  13.     post_content = post_content_d;
  14.     post_start_len = post_start_len_d;
  15.     post_end_len = post_end_len_d;
  16.     post_content_len = post_content_len_d;
  17.     post_len = post_start_len_d + post_content_len_d + post_end_len_d;
  18.     content_type = content_type_d;
  19. }
每次录音成功后再读取文件,更新内容大小相关变量:
  1. /* HttpClient.cpp */
  2. int HttpClient::mysetPostContent(uint8_t *post_content_d, int post_content_len_d, int post_len_d)
  3. {
  4.     post_content = post_content_d;
  5.     post_content_len = post_content_len_d;
  6.     post_len = post_len_d;
  7. }
最后添加方法 mypost(),流程:


  • mystartRequest():


    • mysendInitialHeaders():发送请求头信息。

    • finishHeaders():发送空行结束 header。

  • 发送请求体头部,一边读取音频一边发送,发送请求体尾部。


实现:
  1. /* HttpClient.h */
  2. #define HTTP_CONTENT_TYPE "Content-Type"
  3. /* HttpClient.cpp */
  4. int HttpClient::mysendInitialHeaders(const char *aServerName, IPAddress aServerIP, uint16_t aPort, const char *aURLPath, const char *aHttpMethod, const char *aUserAgent)
  5. {
  6. // 与 sendInitialHeaders() 主要区别部分
  7. sendHeader(HTTP_HEADER_CONNECTION, "keep-alive");
  8. sendHeader(HTTP_HEADER_CONTENT_LENGTH, post_len);
  9. sendHeader(HTTP_CONTENT_TYPE, content_type);
  10. }/* HttpClient.cpp */
  11. int HttpClient::mypost(const char *aServerName, const char *aURLPath, SdFatFile file, const char *aUserAgent)
  12. {
  13.     const int MY_BODY_SIZE = 1000;
  14.     uint8_t buf_temp[MY_BODY_SIZE]; // 读取音频并发送的缓冲
  15.     memset(buf_temp, 0, MY_BODY_SIZE);
  16.     // 发送请求头
  17.     int req_ret = mystartRequest(aServerName, kHttpPort, aURLPath, HTTP_METHOD_POST, aUserAgent);
  18.     if (HTTP_SUCCESS != req_ret)
  19.     {
  20.         return req_ret;
  21.     }
  22.     // 发送请求体头部
  23.     iClient->write((const uint8_t *)post_start, post_start_len);
  24.     // 发送音频文件,可能分多次发送
  25.     int read_bytes = file.read(buf_temp, MY_BODY_SIZE);
  26.     iClient->write(buf_temp, read_bytes);
  27.     while (read_bytes == MY_BODY_SIZE)
  28.     {
  29.         read_bytes = file.read(buf_temp, MY_BODY_SIZE);
  30.         iClient->write(buf_temp, read_bytes);
  31.     }
  32.     // 发送请求体尾部
  33.     iClient->write((const uint8_t *)post_end, post_end_len);
  34.     return HTTP_SUCCESS;
  35. }

FatFs_SD

功能完善可以直接使用。貌似只支持Fat32?需要提前制备 SD 卡。

没有获取文件大小的方法,自己实现:
  1. /* SdFatFile.cpp */
  2. int SdFatFile::size() {
  3.     return f_size((FIL *)m_file);
  4. }

RecordWav

功能完善,可以直接录制生成 Wav 格式的音频文件,不需要手动给 PCM 文件写文件头,能直接发给云端进行语音识别,非常方便。只需要提前设置采样率位深等信息。

主循环

实现了各种方法后就能在主循环内实现云语音识别的流程了:
  1. void loop()
  2. {
  3.     if ((digitalRead(RECORDBTN) == HIGH) && (!recWav.fileOpened()))
  4.     {
  5.         // 按下按钮录制音频
  6.         sprintf(record_file_name, "%d.wav", record_counter);
  7.         sprintf(absolute_filename, "%s%s", fs.getRootPath(), record_file_name);
  8.         Serial.println("Recording started");
  9.         recWav.openFile(absolute_filename);
  10.     }
  11.     else if ((digitalRead(RECORDBTN) == LOW) && (recWav.fileOpened()))
  12.     {   
  13.         // 松开按钮停止录制
  14.         Serial.println("Recording stopped");
  15.         recWav.closeFile();
  16.         
  17.         // 文件保存后再打开文件,获取大小
  18.         SdFatFile record_file = fs.open(absolute_filename);
  19.         int record_file_len = record_file.size();
  20.         printf("size:%d", record_file_len);
  21.         
  22.         // 设置 Post Content 相关变量
  23.         http.mysetPostContent(NULL, record_file_len, post_start_len + record_file_len + post_end_len);
  24.         // POST 音频数据
  25.         err = http.mypost(kHostname, kPath, record_file);
  26.         // 关闭文件句柄
  27.         record_file.close();
  28.         
  29.         // 读取解析 response (省略)
  30.         if (err == 0){...}
  31.         delay(100);
  32. }
服务器

服务器端搭建在我的个人网站上,nginx+PHP架构,使用腾讯云提供的语音识别 api。假设板端将请求提交到 http://asr.hazhuzhu.com/ameba_asr.php

nginx
  1. server
  2. {
  3.     listen 80;
  4.     server_name asr.hazhuzhu.com;
  5.     client_max_body_size 128m;
  6.     root /home/wwwroot/asr;
  7.     index index.html index.htm index.php;
  8.     location / {
  9.         try_files $uri $uri/ =404;
  10.     }
  11.     location ~ \.php$ {
  12.         include fastcgi.conf;
  13.         fastcgi_pass    unix:/tmp/php-cgi.sock;
  14.         fastcgi_keep_conn on;
  15.     }
  16. }
PHP

后端负责存储音频文件并调用语音识别 api(需要使用腾讯云相关 SDK),最后返回识别结果。api 调用部分腾讯云提供了相关文档和代码生成工具,比较方便。
  1. <?php
  2. $uploads_dir = 'ameba_recordings';
  3. if ($_FILES['file']['error'] == UPLOAD_ERR_OK)
  4. {
  5.         $tmp_name = $_FILES['file']['tmp_name'];
  6.         // $name = $_FILES['file']['name'];
  7.         $date_str=date('YmdHis');
  8.         move_uploaded_file($tmp_name, "$uploads_dir/$date_str".'.wav');
  9.         
  10.          try {
  11.          // 调用 api 部分,省略
  12.                 $result_str=$resp->getResult();
  13.                 $result_file=fopen("$uploads_dir/$date_str".'.txt',"a");
  14.                 fwrite($result_file,$result_str);
  15.                 fclose($result_file);
  16.                 echo $result_str;
  17.         }
  18.         catch(TencentCloudSDKException $e) {
  19.                 echo $e;
  20.         }
至此我们完成了一个完整的基于 Ameba RTL8722DM MINI 的云语音识别应用。

总结

首先我想谈谈使用 Ameba RTL8722DM MINI 的开发体验。正如前文所述,MINI 体积小但功能强大,能够实现大部分物联网场景应用。并且 MINI 支持 Arduino 开发,很快就能上手实现,不需要构建繁杂的单片机工程。而且官网的文档非常详尽,大量的示例程序、api文档、硬件细节以及详细的源码注释等,极大地方便了我们的开发。相关工作人员也很有耐心,点赞!

其次,其它开发者有提到麦克风的问题。我感觉确实录制下来的声音比较小,可能是缺省增益不够大?不过最终实现的语音识别效果还不错,没有什么影响。

最后再次感谢 Realtek 与面包板平台提供的这次测评与学习的机会。囿于时间和技术水平,本项目还有许多值得改进的地方,包括代码规范与识别效率等,希望能和大家共同交流学习!也祝主办方的技术生态越来越好!