芯片开放社区
直播中

abdkjshd

8年用户 1156经验值
擅长:可编程逻辑
私信 关注
[问答]

如何去实现基于HTTPClient云语音识别的POST请求功能呢

如何去实现基于HTTPClient云语音识别的POST请求功能呢?有哪些步骤呢?

回帖(1)

薄坤坤

2022-3-8 09:16:42
RVB2601


首先,我们结合之前所作的工作,重新梳理云语音识别项目的目标与思路。


目标:按下板载按钮,RVB2601 开始录音,上传云端语音识别后返回识别结果至串口。


思路:实现的程序流程与所需要的关键组件如下:



  • gpio_pin:设置按键中断。按下按钮后设置“开始录音标志位”为1。
  • rhino:注册监控任务。监控“开始录音标志位”,若为1则:




    • codec:进行录音。
    • HTTPClient :上传录音数据,接收并返回识别结果。
    • 清空标志位


HTTPClient 发送录音数据


首先从最复杂的,手动发送音频数据开始。


借鉴另一个组件 http 的思路,先针对我们的应用场景设置好 boundary 和请求体的头部、尾部:

// 请求体格式
static const char *boundary = "----WebKitFormBoundarypNjgoVtFRlzPquKE";
#define MY_FORMAT_START "------WebKitFormBoundarypNjgoVtFRlzPquKErnContent-Disposition: %s; name="%s"; filename="%s"rnContent-Type: %srnrn"
#define MY_FORMAT_END "rn------WebKitFormBoundarypNjgoVtFRlzPquKE--rn"
由于我们的音频数据较大,且一次流程只发送一个文件,因此发送 POST 请求体时可以分三部分发送:头部、音频数据、尾部。所以在进行音频数据传输时,相比于原来的流程,我们要先设置“Content-Type”,接着基于模板制备请求体头部尾部,设置 Content-Length,并将 POST 相关变量统一存放在一个结构体中,方便进行 POST 请求时调用。以下是音频发送函数:

static void post_audio()
{
    http_client_config_t config = {
        .url = "http://upload.hazhuzhu.com/myasr.php",
        .method = HTTP_METHOD_POST,
        .event_handler = _http_event_handler,
    };

    http_client_handle_t client = http_client_init(&config);
    // 设置 Content-Type
    http_client_set_header(client, "Content-Type", "multipart/form-data; boundary=----WebKitFormBoundarypNjgoVtFRlzPquKE");

    const unsigned char *post_content = repeater_data_addr; // POST 请求体的内容为录音数据
    // 请求体格式相关变量
    const char *content_disposition = "form-data";
    const char *name = "file";
    const char *filename = "raw_recording";
    const char *content_type = "application/octet-stream";
    int boundary_len = strlen(boundary);

    // 制备请求体头部
    int post_start_len = strlen(MY_FORMAT_START) - 8 + strlen(content_disposition) + strlen(name) + strlen(filename) + strlen(content_type) + 1;
    char *post_start = (char *)malloc(post_start_len + 1);
    memset(post_start, 0, sizeof(post_start_len));
    snprintf(post_start, post_start_len, MY_FORMAT_START, content_disposition, name, filename, content_type);
    const char *post_end = MY_FORMAT_END;
    // 设置 Content-Length
    http_client_set_post_len(client, strlen(post_start) + PCM_LEN + strlen(post_end));

    // 设置请求体相关变量
    my_post_data_t post_data = {
        .start = post_start,
        .end = post_end,
        .content = post_content,
        .start_len = strlen(post_start),
        .end_len = strlen(post_end),
        .content_len = PCM_LEN,
    };

    // 进行一次 POST 请求
    http_errors_t err = http_client_myperform(client, &post_data);

    if (err == HTTP_CLI_OK)
    {
        LOGI(TAG, "HTTP POST Status = %d, content_length = %d rn",
             http_client_get_status_code(client),
             http_client_get_content_length(client));
        char *raw_data_p;
        result_len = http_client_get_response_raw_data(client, &raw_data_p);
        // 输出 response
        if (result_len)
        {
            char *result = (char *)malloc(result_len + 1);
            memcpy(result, raw_data_p, result_len);
            memcpy(result + result_len, "", 1);
            printf("%drn", result_len);
            printf("%srn", result);
            result_len = 0;
            free(result);
        }
    }
    else
    {
        LOGE(TAG, "HTTP POST request failed: 0x%x @#@@@@@@", (err));
        e_count++;
    }

    http_client_cleanup(client);
}
其中,http_client_myperform() 是将 http_client_perform() 中的 http_client_send_post_data() 替换为 http_client_mysend_post_data():

/* http_client.c */

#define MY_BODY_SIZE 1000

static web_err_t http_client_mysend_post_data(http_client_handle_t client, my_post_data_t *post_data)
{
    // 此时请求头应发送完毕
    if (client->state != HTTP_STATE_REQ_COMPLETE_HEADER)
    {
        LOGE(TAG, "Invalid state");
        return WEB_ERR_INVALID_STATE;
    }
    // 没有要发送的 post_data 直接返回(包括其它请求)
    if (!(post_data->content && client->post_len))
    {
        goto success;
    }

    // 发送请求体头部
    int wret = http_client_write(client, post_data->start, post_data->start_len);
    if (wret < 0)
    {
        return wret;
    }
    client->data_write_left -= wret;

    // 发送音频数据,可能发送多次
    int content_idx = 0;
    for (int i = 0; i < post_data->content_len / MY_BODY_SIZE; ++i)
    {
        wret = http_client_write(client, post_data->content + content_idx, MY_BODY_SIZE);
        if (wret < 0)
        {
            return wret;
        }
        client->data_write_left -= wret;
        content_idx += MY_BODY_SIZE;
    }
    wret = http_client_write(client, post_data->content + content_idx, post_data->content_len - content_idx);
    if (wret < 0)
    {
        return wret;
    }
    client->data_write_left -= wret;

    // 发送请求体尾部
    wret = http_client_write(client, post_data->end, post_data->end_len);
    if (wret < 0)
    {
        return wret;
    }
    client->data_write_left -= wret;

    // 发送完毕
    if (client->data_write_left <= 0)
    {
        goto success;
    }
    else
    {
        return ERR_HTTP_WRITE_DATA;
    }

success:
    // 更新状态
    client->state = HTTP_STATE_REQ_COMPLETE_DATA;
    return WEB_OK;
}
相比于原来只发送一次数据,现在分三部分发送。并且,由于网络库提供的 http_client_write() 函数没有考虑 TCP 数据包大小限制,我们手动将音频数据每次 MY_BODY_SIZE 大小,多次发送:


http_client_write() 调用流程:



  • transport_write()
  • _write()
  • select()


整个流程都没有考虑数据包大小,若一次发送数据过大会阻塞,因此需要手动分包。


http_client_myperform() 的其它流程都不需要修改,接收到 response 后库函数会自动解析。阅读源码后发现最后会将服务器端发回的原始数据放在 client->response->buffer->raw_data 里。我们写一个函数提取出来:

/* http_client.c */

int http_client_get_response_raw_data(http_client_handle_t client, char **raw_data)
{
    int raw_len = client->response->buffer->raw_len;
    if (raw_len)
    {
        *raw_data = client->response->buffer->raw_data;
        return raw_len;
    }
    return 0;
}
在 post_audio() 的最后我们会调用这个函数,并将识别结果打印出来。

codec 录制音频


录音部分仿照 ch2601_ft_demo 写。由于 RVB2601 存储资源有限,我们只开辟 49152 字节的录音缓存(不能初始化赋值,否则 flash 装不下)。设置采样率 8000Hz,位深 16bit(更清晰方便识别)。这样大概能录制 1.536s,对于我们演示一些简短的口令来说也够用了。


事实上我们后面要调用的语音识别 api 要求音频文件是单声道的,我们后面还需要进行转换。所以这里其实存在一定的存储空间浪费,但我没有在库里找到只录制单声道音频的方法……默认就是双声道的……

/* 录音 */
static void cmd_mic_handler()
{
    csi_error_t ret;
    csi_codec_input_config_t input_config;
    ret = csi_codec_init(&codec, 0);

    if (ret != CSI_OK)
    {
        printf("csi_codec_init errorn");
        return;
    }

    codec_input_ch.ring_buf = &input_ring_buffer;
    csi_codec_input_open(&codec, &codec_input_ch, 0);
    /* input ch config */
    csi_codec_input_attach_callback(&codec_input_ch, codec_input_event_cb_fun, NULL);
    input_config.bit_width = 16;
    input_config.sample_rate = 8000;
    input_config.buffer = input_buf;
    input_config.buffer_size = INPUT_BUF_SIZE;
    input_config.period = 1024;
    input_config.mode = CODEC_INPUT_DIFFERENCE;
    csi_codec_input_config(&codec_input_ch, &input_config);
    csi_codec_input_analog_gain(&codec_input_ch, 0xbf);
    csi_codec_input_link_dma(&codec_input_ch, &dma_ch_input_handle);

    printf("start recodern");
    csi_codec_input_start(&codec_input_ch);

    // 麦克风录音写入数据 48x1024=49152
    while (new_data_flag < 48)
    {
        if (cb_input_transfer_flag)
        {
            csi_codec_input_read_async(&codec_input_ch, repeater_data_addr + (new_data_flag * 1024), 1024);
            cb_input_transfer_flag = 0U; // 回调函数将其置 1
            new_data_flag++;
        }
    }

    new_data_flag = 0;

    printf("stop recodern");
    csi_codec_input_stop(&codec_input_ch);
    csi_codec_input_link_dma(&codec_input_ch, NULL);
    csi_codec_input_detach_callback(&codec_input_ch);
    csi_codec_uninit(&codec);

    return;
}按键中断


初始化时设置按键中断,并在回调函数里设置标志位:

static void gpio_pin_callback(csi_gpio_pin_t *pin, void *arg)
{
    start_to_record = 1;    // 标志位置 1 通知 mic 任务开始录音并上传
}

/* Key1 初始化 */
int btn_init()
{
    // key 1
    memset(&g_handle, 0, sizeof(g_handle));
    csi_pin_set_mux(PA11, PIN_FUNC_GPIO);
    csi_gpio_pin_init(&g_handle, PA11);
    csi_gpio_pin_dir(&g_handle, GPIO_DIRECTION_INPUT);
    csi_gpio_pin_mode(&g_handle, GPIO_MODE_PULLUP);
    csi_gpio_pin_debounce(&g_handle, true);
    csi_gpio_pin_attach_callback(&g_handle, gpio_pin_callback, &g_handle);
    csi_gpio_pin_irq_mode(&g_handle, GPIO_IRQ_MODE_FALLING_EDGE);
    csi_gpio_pin_irq_enable(&g_handle, true);

    return 0;
}监控任务


编写监控任务并注册。我也写了 cli 命令,可以通过串口控制:

// 监控任务
static void mic_task(void *arg)
{
    while (1)
    {
        // 按下按钮后录制并上传
        if (start_to_record == 1)
        {
            cmd_mic_handler();
            post_audio();
            start_to_record = 0;
        }
        aos_msleep(100);
    }
}


static void cmd_http_func(char *wbuf, int wbuf_len, int argc, char **argv)
{
    if (argc == 2 && strcmp(argv[1], "post") == 0)
    {
        post_audio();
    }
    else
    {
        printf("thttp postn");
    }
}

static void cmd_mic_func(char *wbuf, int wbuf_len, int argc, char **argv)
{
    if (argc == 2 && strcmp(argv[1], "record") == 0)
    {
        cmd_mic_handler();
    }
    else
    {
        printf("tmic recordn");
    }
}

int cli_reg_cmd_asr(void)
{
    char url[128];

    // POST 录音
    static const struct cli_command http_cmd_info = {
        "http",
        "http post",
        cmd_http_func,
    };

    // 录音命令
    static const struct cli_command mic_cmd_info = {
        "mic",
        "mic record",
        cmd_mic_func,
    };

    // 注册命令
    aos_cli_register_command(&http_cmd_info);
    aos_cli_register_command(&mic_cmd_info);

    // 新建麦克风任务
    aos_task_new("mic", mic_task, NULL, 10 * 1024);

    return 0;
}修改链接文件


如果编译中遇到 SRAM overflowed 的问题,可以修改 configs/gcc_flash.ld:

- REGION_ALIAS("REGION_BSS",     SRAM);
+ REGION_ALIAS("REGION_BSS",     DSRAM);
将 BSS 段存到 DSRAM。ch2601_webplayer_demo 工程中的 linker file 和默认的不一样,不知道为什么……


这样,板端就基本开发完毕了。本来打算将识别结果显示在 oled 上的,然而 flash 不够用了……

服务器


nginx+PHP架构,使用腾讯云提供的语音识别 api。

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 调用部分腾讯云提供了相关文档和代码生成工具,比较方便。


由于 RVB2601 codec 库存储的录音数据是 PCM 编码,2通道。而 api 只接收 wav/mp3 ,1通道的音频文件。而且,由于我们在 http_client_mysend_post_data() 中手动发送音频时没有考虑大小端的问题,所以服务器接收到我们 16bit 音频还是大端存储的。所以在调用 api 前,还需要修正大小端、通道数和文件格式。处于效率,直接调用 ffmepg 来实现……


// 调用腾讯云 SDK 省略
$uploads_dir = 'ch2601_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".'-b2.pcm');
    // 用 ffmpeg 转码,先转大小端并压缩为 1 通道,再转为 wav 格式
    exec('ffmpeg -f s16be -ar 8000 -ac 2 -i '."$uploads_dir/$date_str".'-b2.pcm'.' -f s16le -ar 8000 -ac 1 '."$uploads_dir/$date_str".'-l1.pcm');
    exec('ffmpeg -f s16le -ar 8000 -ac 1 -i '."$uploads_dir/$date_str".'-l1.pcm '."$uploads_dir/$date_str".'-l1.wav');

    try {
        // api 调用部分,省略……
        $resp = $client->SentenceRecognition($req);

        $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;
    }
}
至此我们完成了一个完整的基于 RVB2601 的云语音识别应用。

总结


首先我想谈谈使用 RVB2601 的开发体验。正如前文所述,RVB2601 板载资源丰富,能够实现大部分物联网场景应用。并且剑池 CDK 也和我们熟悉的其它单片机开发 IDE 类似,很容易就能上手。RVB2601 的几个示例程序也能让我们很快熟悉板子的开发步骤。RVB2601 有 RST 键,下载后不需要反复插拔。工作人员也非常热心认真,在我发现组件网络资源有点问题的时候几分钟就回应并修复了。
最重要的是,YOC 功能非常强大。还有太多的 API 与组件我还没有尝试,但我相信如果能熟练运用,我们很容易就能基于 YOC 支持芯片上完成一个完整并强大的物联网系统。
举报

更多回帖

×
20
完善资料,
赚取积分