基于 Realtek RTL8722DM MINI 的云语音识别

前言

本项目旨在实现基于 Realtek RTL8722DM MINI 的云语音识别,主要工作在于拓展官方网络库 HTTPClient,将音频数据上传至 HTTP 服务器。思路和前篇文章一样,但好写多了……

项目源码 演示视频

Ameba RTL8722DM MINI 板载功能丰富,支持 Wifi,蓝牙,Codec,麦克风耳机孔,Micro SD卡接口等……因此我们也可以在这块板子上实现云语音识别。

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

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

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

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

  1. 按键检测,利用 RecordWav 录制音频到 SD 卡。
  2. 利用 FatFs_SD 读取刚录制的音频。
  3. 利用 HttpClient 将音频 POST 到服务器,服务器调用语音识别 api 后将识别结果返回给 Ameba。最后返回流程1。

开发流程

Ameba

HttpClient

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

startRequest() 调用:

1. sendInitialHeaders():发送请求头信息,包括“Connection: close”

/* HttpClient.cpp */
// We don't support persistent connections, so tell the server to
// close this connection after we're done
sendHeader(HTTP_HEADER_CONNECTION, "close");

2. finifinishHeaders():发送空行结束 header

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

POST 二进制流类型的文件对 POST 请求头和请求体的格式具有一定要求,我们只需要按照要求的格式和流程发送数据即可。由于我们一次流程只发送一个语音文件,并且其体积较大,因此请求体中只有头部及尾部的两个 boundary,以及中间的音频内容。因此我们可以提前设置好两个 boundary,以及 Content_Type, Content-Length 等内容以制作请求头,并在请求体中分三个部分发送即可:

/* speech_recognizer.ino */
char Content_Type[] = "multipart/form-data; boundary=----WebKitFormBoundarypNjgoVtFRlzPquKE";
// 请求体头部及尾部数据
char post_start[] = "------WebKitFormBoundarypNjgoVtFRlzPquKE\r\nContent-Disposition: form-data; name=\"file\"; filename=\"ameba_recording\"\r\nContent-Type: application/octet-stream\r\n\r\n";
char post_end[] = "\r\n------WebKitFormBoundarypNjgoVtFRlzPquKE--\r\n";
const int post_start_len = strlen(post_start);
const int post_end_len = strlen(post_end);

在 setup() 中将它们写入我们在 HttpClient 中添加的成员变量中。在本项目中需要一边读取 sd 卡一边发送,因此不提前设置 post_content。

/* speech_recognizer.ino */
void setup()
{
    uint8_t *post_content = NULL;
    http.mysetPostData(post_start, post_end, post_content, post_start_len, post_end_len, 0, Content_Type);
}

/* HttpClient.cpp */
int HttpClient::mysetPostData(char *post_start_d, char *post_end_d, uint8_t *post_content_d, int post_start_len_d,
                              int post_end_len_d, int post_content_len_d, char *content_type_d)
{
    post_start = post_start_d;
    post_end = post_end_d;
    post_content = post_content_d;
    post_start_len = post_start_len_d;
    post_end_len = post_end_len_d;
    post_content_len = post_content_len_d;
    post_len = post_start_len_d + post_content_len_d + post_end_len_d;
    content_type = content_type_d;
}

每次录音成功后再读取文件,更新内容大小相关变量:

/* HttpClient.cpp */
int HttpClient::mysetPostContent(uint8_t *post_content_d, int post_content_len_d, int post_len_d)
{
    post_content = post_content_d;
    post_content_len = post_content_len_d;
    post_len = post_len_d;
}

最后添加方法 mypost(),流程:

  1. mystartRequest():
    1. mysendInitialHeaders():发送请求头信息。
    2. finishHeaders():发送空行结束 header。
  2. 发送请求体头部,一边读取音频一边发送,发送请求体尾部。

实现:

/* HttpClient.h */
#define HTTP_CONTENT_TYPE "Content-Type"

/* HttpClient.cpp */
int HttpClient::mysendInitialHeaders(const char *aServerName, IPAddress aServerIP, uint16_t aPort, const char *aURLPath, const char *aHttpMethod, const char *aUserAgent)
{
// 与 sendInitialHeaders() 主要区别部分
sendHeader(HTTP_HEADER_CONNECTION, "keep-alive");
sendHeader(HTTP_HEADER_CONTENT_LENGTH, post_len);
sendHeader(HTTP_CONTENT_TYPE, content_type);
}
/* HttpClient.cpp */
int HttpClient::mypost(const char *aServerName, const char *aURLPath, SdFatFile file, const char *aUserAgent)
{
    const int MY_BODY_SIZE = 1000;
    uint8_t buf_temp[MY_BODY_SIZE]; // 读取音频并发送的缓冲
    memset(buf_temp, 0, MY_BODY_SIZE);

    // 发送请求头
    int req_ret = mystartRequest(aServerName, kHttpPort, aURLPath, HTTP_METHOD_POST, aUserAgent);
    if (HTTP_SUCCESS != req_ret)
    {
        return req_ret;
    }

    // 发送请求体头部
    iClient->write((const uint8_t *)post_start, post_start_len);

    // 发送音频文件,可能分多次发送
    int read_bytes = file.read(buf_temp, MY_BODY_SIZE);
    iClient->write(buf_temp, read_bytes);
    while (read_bytes == MY_BODY_SIZE)
    {
        read_bytes = file.read(buf_temp, MY_BODY_SIZE);
        iClient->write(buf_temp, read_bytes);
    }

    // 发送请求体尾部
    iClient->write((const uint8_t *)post_end, post_end_len);

    return HTTP_SUCCESS;
}

FatFs_SD

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

没有获取文件大小的方法,自己实现:

/* SdFatFile.cpp */
int SdFatFile::size() {
    return f_size((FIL *)m_file);
}

RecordWav

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

主循环

实现了各种方法后就能在主循环内实现云语音识别的流程了:

void loop()
{
    if ((digitalRead(RECORDBTN) == HIGH) && (!recWav.fileOpened()))
    {
        // 按下按钮录制音频
        sprintf(record_file_name, "%d.wav", record_counter);
        sprintf(absolute_filename, "%s%s", fs.getRootPath(), record_file_name);
        Serial.println("Recording started");
        recWav.openFile(absolute_filename);
    }
    else if ((digitalRead(RECORDBTN) == LOW) && (recWav.fileOpened()))
    {   
        // 松开按钮停止录制
        Serial.println("Recording stopped");
        recWav.closeFile();

        // 文件保存后再打开文件,获取大小
        SdFatFile record_file = fs.open(absolute_filename);
        int record_file_len = record_file.size();
        printf("size:%d", record_file_len);

        // 设置 Post Content 相关变量
        http.mysetPostContent(NULL, record_file_len, post_start_len + record_file_len + post_end_len);
        // POST 音频数据
        err = http.mypost(kHostname, kPath, record_file);
        // 关闭文件句柄
        record_file.close();

        // 读取解析 response (省略)
        if (err == 0){...}
        delay(100);
}

服务器

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

nginx

server
{
    listen 80;
    server_name asr.hazhuzhu.com;

    client_max_body_size 128m;

    root /home/wwwroot/asr;
    index index.html index.htm index.php;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include fastcgi.conf;
        fastcgi_pass    unix:/tmp/php-cgi.sock;
        fastcgi_keep_conn on;
    }
}

PHP

后端负责存储音频文件并调用语音识别 api(需要使用腾讯云相关 SDK),最后返回识别结果。api 调用部分腾讯云提供了相关文档和代码生成工具,比较方便。

<?php
$uploads_dir = 'ameba_recordings';

if ($_FILES['file']['error'] == UPLOAD_ERR_OK)
{
        $tmp_name = $_FILES['file']['tmp_name'];
        // $name = $_FILES['file']['name'];
        $date_str=date('YmdHis');
        move_uploaded_file($tmp_name, "$uploads_dir/$date_str".'.wav');

         try {
         // 调用 api 部分,省略
                $result_str=$resp->getResult();
                $result_file=fopen("$uploads_dir/$date_str".'.txt',"a");
                fwrite($result_file,$result_str);
                fclose($result_file);
                echo $result_str;
        }
        catch(TencentCloudSDKException $e) {
                echo $e;
        }

至此我们完成了一个完整的基于 Ameba RTL8722DM MINI 的云语音识别应用。

总结

用 Ameba RTL8722DM MINI 开发舒服多了……一方面可能是因为 Arduino 本身就比较简洁,另一方面官方文档非常详细,还有大量的示例程序、api文档、硬件细节以及详细的源码注释等……在之前项目的基础上,花了半天多点就写好了……

文章作者:哈猪猪
文章链接:https://hazhuzhu.com/mcu/rtl8722dm-mini-speech_recognizer.html
许可协议: CC BY-NC-SA 4.0
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇