本帖最后由 hazhuzhu 于 2022-2-28 20:11 编辑
前言
在文章2中,我们简单分析了 HTTPClient 组件的源码,发现其并不能实现“mul tipart/form-data”类型内容的 POST 请求。因此在本文中,我们将手动实现这一功能,并且最终完成整个云语音识别项目。以下为全部开发流程:
实现效果:见文章4顶部视频。 RVB2601
首先,我们结合之前所作的工作,重新梳理云语音识别项目的目标与思路。
目标:按下板载按钮,RVB2601 开始录音,上传云端语音识别后返回识别结果至串口。
思路:实现的程序流程与所需要的关键组件如下: 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, "\0", 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_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 命令,可以通过串口控制:
修改链接文件
如果编译中遇到 SRAM overflowed 的问题,可以修改 configs/gcc_flash.ld:
- REGION_ALIAS("REGION_BSS", SRAM);
复制代码
将 BSS 段存到 DSRAM。ch2601_webplayer_demo 工程中的 linker file 和默认的不一样,不知道为什么……
这样,板端就基本开发完毕了。本来打算将识别结果显示在 oled 上的,然而 flash 不够用了……
由于发烧友文章长度限制,服务器端代码与总结放在文章4……
0
|
|
|
|