技术标签: esp-idf esp32-s3 CMake Funpack HTTPS 嵌入式同好会 语音合成 esp-box-lite
【Funpack2-5】基于ESP32-S3-BOX-Lite的语音合成与播报系统
Github: EmbeddedCamerata/esp-box-lite-bfans-tts
本项目基于ESP32-S3-BOX-Lite,使用esp-idf开发,连接WiFi并发出HTTPS请求,返回B站用户数据信息,再使用cJSON完成json数据解析,得到用户粉丝数,最后通过TTS实现语音合成与播报。
Github: espressif/esp-box
Github: espressif/esp-idf
ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。该开发板配备一块2.4寸LCD显示屏、双麦克风、一个扬声器、两个用于硬件拓展的Pmod兼容接口、结合三个独立按键,可构建多样的HMI人机交互应用。
ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。
强烈建议采用esp-box仓库中所述的稳定版本:esp-idf v4.4.4
以及 esp-box v0.3.0
。二者环境的安装及基本例程上手参见各说明文档,在此不作赘述。
工程目录上,可仿照乐鑫提供的 模板 构建自己的工程目录。例如:
├ build(编译时所产生的构建文件)
├ main
│ ├ include
│ │ ├ wifi_connect.h
│ │ └ ...(其余头文件)
│ └ src
│ │ ├ wifi_connect.c
│ │ └ ...(其余源代码)
│ ├ main.c
│ ├ CMakeLists.txt
│ └ component.mk
├ resources
│ └ server_root_cert.pem(HTTPS请求所用到的网站CA root cert)
├ CMakeLists.txt
├ Makefile
├ partitions.csv
└ ...
️ 注意:工程根目录下 CMakeLists.txt
中,EXTRA_COMPONENT_DIRS
需要指定本地esp-box仓库所在路径,例如:
cmake_minimum_required(VERSION 3.5)
include($ENV{
IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS ../esp-box/components)
project(esp-box-lite-bfans-tts)
参考esp-box中的示例,需要注意的是由于会用到TTS,调用 bsp_board_power_ctrl()
打开AUDIO电源是必要的。
ESP_ERROR_CHECK(bsp_board_init());
ESP_ERROR_CHECK(bsp_board_power_ctrl(POWER_MODULE_AUDIO, true));
参考esp-idf所提供的有关wifi的示例:station_example_main,为wifi功能单独设置 .c/.h
文件实现。将示例中的宏定义修改为实际值:
#define EXAMPLE_ESP_WIFI_SSID YOUR_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS YOUR_WIFI_PASSWORD
#define EXAMPLE_ESP_MAXIMUM_RETRY 3
在工程的主程序 main.c
中,直接使用示例的主程序即可:
/* Initialize NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
wifi_init_sta();
️ 注意:在初始化wifi协议栈之前,需初始化NVS(非易失性存储单元)。由于乐鑫会将SSID与密码存储在NVS中,因此需要对开发板做分区配置 partitions.csv
,根据esp-box中的例程,增加一个nvs分区与voice_data分区(TTS会用到):
# Name | Type | SubType | Offset | Size |
---|---|---|---|---|
nvs | data | nvs | 0x9000 | 0x4000 |
factory | app | factory | 0x010000 | 6M |
voice_data | data | fat | 0x610000 | 3890K |
该部分完成对 api.bilibili.com 发出HTTPS请求,URL为:https://api.bilibili.com/x/relation/stat?vmid=
+ user_uid
。
参考esp-idf所提供的有关HTTPS请求的示例:https_request_example_main,为该功能单独设置 .c/.h
文件实现。将示例中的宏定义修改为实际值:
#define WEB_SERVER "api.bilibili.com"
#define WEB_PORT "443"
#define WEB_URL "https://api.bilibili.com/x/relation/stat?vmid=user_uid" // 用户B站UID
在主程序中,由于WiFi功能的初始化已经为我们完成了NVS、netif、event_loop_create等初始化操作,因此只需要示例主程序中创建线程的操作即可:
xTaskCreate(&https_request_task, "https_get_task", 8192, NULL, 5, NULL);
️ 注意:HTTPS请求,需要网站的CA根证书文件 .pem
。在示例中有注释指导如何生成网站的 .pem
文件:
/* Root cert for howsmyssl.com, taken from server_root_cert.pem
The PEM file was extracted from the output of this command:
openssl s_client -showcerts -connect www.howsmyssl.com:443 </dev/null
The CA root cert is the last cert given in the chain of certs.
To embed it in the app binary, the PEM file is named
in the component.mk COMPONENT_EMBED_TXTFILES variable.
*/
生成 .pem
文件后,修改工程目录下的 component.mk
,将 .pem
文件以二进制形式储存(笔者将其单独放在工程根目录下 .resources
目录下):
COMPONENT_EMBED_TXTFILES := ../resources/server_root_cert.pem
所返回的请求数据(字符串)形如:
HTTP/1.1 200 OK
Date: Sun, 16 Jul 2023 15:33:55 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 108
Connection: keep-alive
Bili-Status-Code: 0
Bili-Trace-Id: 550bd14e7a64b40d
X-Bili-Trace-Id: 26617a088e651658550bd14e7a64b40d
Expires: Sun, 16 Jul 2023 15:33:54 GMT
Cache-Control: no-cache
X-Cache-Webcdn: BYPASS from blzone04
{"code":0,"message":"0","ttl":1,"data":{"mid":42602419,"following":243,"whisper":0,"black":0,"follower":69}}
其中json字段的 data
中 follower
是想解析得到的数据。由于TTS将会用到返回的数据,因此在每次获取到返回数据后,用 memcpy
将其保存至全局变量 https_req_buf
。
#define MAX_REQUEST_BUF_LEN 512
char https_req_buf[MAX_REQUEST_BUF_LEN];
static void https_get_request(esp_tls_cfg_t cfg, const char *WEB_SERVER_URL, const char *REQUEST)
{
...
/* Print response directly to stdout as it is read */
for (int i = 0; i < len; i++)
{
putchar(buf[i]);
}
putchar('\n'); // JSON output doesn't have a newline at end
memcpy(https_req_buf, buf, MAX_REQUEST_BUF_LEN); // 保存至全局变量内
} while (1);
...
}
esp-idf/components下,已经包含 cJSON 库,用于做json数据的解析或合成。在上一节已经知道了API返回的数据,因此先简单做个字符串截断 strchr(buf, '{')
,待获取到json数据后,再提取json中 follower
字段的内容。
esp_err_t json_parse_followers(char **parsed)
{
char *json_buf;
cJSON *cjson_root = NULL;
json_buf = strchr(https_req_buf, '{');
cjson_root = cJSON_Parse(json_buf);
cJSON *cjson_data = cJSON_GetObjectItem(cjson_root, "data");
cJSON *cjson_follower = cJSON_GetObjectItem(cjson_data, "follower");
*parsed = (char*)malloc(32*sizeof(char));
*parsed = cJSON_Print(cjson_follower);
cJSON_Delete(cjson_root);
return ESP_OK;
}
// 外部调用方式
char *followers;
esp_err_t err = json_parse_followers(&followers);
️ 在此输入参数类型为 char **
;使用cJSON的最后,调用 cJSON_Delete()
释放指针。
经过json解析后得到的粉丝数数据,交由TTS模块完成语音合成。首先需要为板子进行TTS功能的初始化。
参考esp-skainet所提供的有关TTS的示例:chinese_tts
esp_tts_voice_data_xiaole.dat
),参考chinese_tts/README,工程源码目录下 ./main/CMakeLists.txt
需要添加如下内容:set(voice_data_image ${
PROJECT_DIR}/../esp-box/components/esp-sr/esp-tts/esp_tts_chinese/esp_tts_voice_data_xiaole.dat)
add_custom_target(voice_data ALL DEPENDS ${
voice_data_image})
add_dependencies(flash voice_data)
partition_table_get_partition_info(size "--partition-name voice_data" "size")
partition_table_get_partition_info(offset "--partition-name voice_data" "offset")
if("${size}" AND "${offset}")
esptool_py_flash_to_partition(flash "voice_data" "${voice_data_image}")
else()
set(message "Failed to find model in partition table file"
"Please add a line(Name=voice_data, Type=data, Size=3890K) to the partition file.")
endif()
️ 注意:修改 voice_data_image
为实际对应 esp_tts_voice_data_xiaole.dat
路径,其余可照抄。
static const char *TAG = "TTS report";
static esp_tts_handle_t *tts_handle;
esp_err_t tts_init()
{
/* 1. create esp tts handle */
// initial voice set from separate voice data partition
const esp_partition_t *part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "voice_data");
if (part == NULL)
{
ESP_LOGE(TAG, "Couldn't find voice data partition!\n");
return ESP_FAIL;
}
else
{
ESP_LOGI(TAG, "voice_data paration size:%d\n", part->size);
}
void *voicedata;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
esp_partition_mmap_handle_t mmap;
esp_err_t err = esp_partition_mmap(part, 0, part->size, ESP_PARTITION_MMAP_DATA, &voicedata, &mmap);
#else
spi_flash_mmap_handle_t mmap;
esp_err_t err = esp_partition_mmap(part, 0, part->size, SPI_FLASH_MMAP_DATA, &voicedata, &mmap);
#endif
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Couldn't map voice data partition!\n");
return ESP_FAIL;
}
esp_tts_voice_t *voice = esp_tts_voice_set_init(&esp_tts_voice_xiaole, (int16_t *)voicedata);
tts_handle = esp_tts_create(voice);
if (!tts_handle)
{
ESP_LOGE(TAG, "Created tts_handle failed!\n");
return ESP_FAIL;
}
return ESP_OK;
}
️ 注意:使用的是xiaole语音数据而非template,esp_tts_voice_t *voice = esp_tts_voice_set_init(&esp_tts_voice_xiaole, (int16_t *)voicedata)
参考esp-sr/speech_command_recognition所提供的简单的语音合成示例:
esp_err_t tts_report(char *prompt, unsigned int speed)
{
if (esp_tts_parse_chinese(tts_handle, prompt))
{
int len[1] = {
0};
size_t bytes_write = 0;
do
{
short *pcm_data = esp_tts_stream_play(tts_handle, len, speed);
i2s_write(I2S_NUM_0, pcm_data, len[0] * 2, &bytes_write, portMAX_DELAY);
} while (len[0] > 0);
}
else
{
ESP_LOGE(TAG, "Parse %s failed!\n", prompt);
return ESP_FAIL;
}
esp_tts_stream_reset(tts_handle);
return ESP_OK;
}
️ 注意:调用 i2s_write()
而非 esp_audio_play()
完成数据写入外设。
其次,还需要调节语音播报语速。即便 esp_tts_stream_play()
函数输入 speed=0
,其语音播报速度依旧太快,需要在开发板BSP相关代码做修改。具体修改 components/bsp/src/boards/esp32_s3_box_lite.c 中139~140行,将I2S与语音编解码时钟频率降低。尝试取11K时语速合适。
// ESP_ERROR_CHECK(bsp_i2s_init(I2S_NUM_0, 16000));
// ESP_ERROR_CHECK(bsp_codec_init(AUDIO_HAL_16K_SAMPLES));
ESP_ERROR_CHECK(bsp_i2s_init(I2S_NUM_0, 11000));
ESP_ERROR_CHECK(bsp_codec_init(AUDIO_HAL_11K_SAMPLES));
将TTS语音合成与播报的功能绑定在按键回调上。ESP32-S3-BOX-Lite正面三个按钮从左至右分别为 BOARD_BTN_ID_PREV
、BOARD_BTN_ID_ENTER
、BOARD_BTN_ID_NEXT
。参考esp-box出厂程序中按键的实现,基本的按键回调函数绑定为:
bsp_btn_register_callback(BOARD_BTN_ID_PREV, BUTTON_PRESS_DOWN, tts_report_cb, NULL);
含义为按下正面左侧按钮时,触发 tts_report_cb
函数。该函数完成对HTTPS请求返回数据的json格式解析、将得到的粉丝数(字符串)显示在屏幕上、将粉丝数字符串转换为字符串,例如,“粉丝数一百一十四”、“粉丝数一千九百一十九”、最后调用 tts_report()
完成语音播报。
void tts_report_cb(void *arg)
{
char *followers;
char prompt[64];
/* Parse the followers number(string) */
esp_err_t err = json_parse_followers(&followers);
/* Update the followers number on the screen */
lvgl_display_update(atoi(followers));
/* Convert the string to Chinese prompt */
err = followers_to_prompt(followers, prompt);
/* Play the prompt at speed=1 */
err = tts_report(prompt, 1);
}
esp-box内已经实现LVGL的移植,可参考出厂程序示例学习LVGL用法。首先,需要在主程序中初始化LVGL:
ESP_ERROR_CHECK(lv_port_init());
bsp_lcd_set_backlight(true);
之后简单地在屏幕中央打印出“Followers: 102”即可,先初始化显示“Followers: 0”:
void lvgl_display_init()
{
/* Change the active screen's background color */
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x003a57), LV_PART_MAIN);
/* Create a white label, set its text and align it to the center */
label = lv_label_create(lv_scr_act());
lv_obj_set_style_text_font(label, &lv_font_montserrat_24, 0); // Font size 24
lv_label_set_text(label, "Followers: 0");
lv_obj_set_style_text_color(lv_scr_act(), lv_palette_main(LV_PALETTE_ORANGE), LV_PART_MAIN);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
}
后在按键回调中, lvgl_display_update()
函数内刷新显示的粉丝数:
void lvgl_display_update(int num)
{
lv_label_set_text_fmt(label, "Followers: %d", num);
}
并且,在工程主程序的最后:
do
{
lv_task_handler();
} while (vTaskDelay(1), true);
通过 idf.py flash monitor
完成程序下载与监控。初始屏幕显示:
之后,开发板将连接上wifi,发出HTTPS请求并返回如下图所示数据:
之后,按下正面左键,开发板将合成语音并播报,如下图所示,“粉丝数六十九”:
同时,屏幕将更新显示:
详细展示参见:B站:基于ESP32-S3-BOX-Lite的语音合成与播报系统
本次工程基于 esp-idf 开发,实现了 WiFi 连接、HTTPS请求,实现B站用户粉丝数的读取,并通过 cJSON 完成 json 数据的解析,最后通过 TTS 实现语音合成与播报。
esp-idf 开发框架旨在基础地、详尽地操控整个芯片与板级的外设与配置。基于 esp-idf 的工程构建,使用 CMake 配置工程与资源的方式非常优雅,在 Linux 上编译速度快(因为Windows 上编译的速度更慢)。esp-idf 提供的各种外设示例也都能参考。TTS 这边,一开始是想参考 esp-skainet 那边基于 esp32-s3-box 的示例,但好像有关音频 codec 的移植似乎有些混乱。期望 esp-skainet 那边能官方支持 esp32-s3-box-lite。
文章浏览阅读79次。打开cmd快捷键 window键+RCMD命令锦集 1. gpedit.msc-----组策略 2. sndrec32-------录音机 3. Nslookup-------IP地址侦测器 ,是一个 监测网络中 DNS 服务器是否能正确实现域名解析的命令行工具。 它在 Windows NT/2000/XP 中均可使用 , 但在 Wi...
文章浏览阅读1.4k次。dwm官网:https://dwm.suckless.org/dwm是一个简洁的平铺式窗口管理器配置简单,使用便捷,没有多少依赖,占用内存非常小总之dwm正合口味安装方法首先在官网下载dwm.tar.gz并解压得到这些东西:BUGS config.mk drw.h dwm.c dwm.png Makefile...我们主要来编辑config.h来进行一些配置和编辑config.mk来正确编译对co..._xxrudwm
文章浏览阅读814次。option = { series: [ { type: "gauge", startAngle: 190, endAngle: -10, min: 0, max: 100, axisLine: { lineStyle: { width: 15, color: [ [0.3, "#91BB7E".._echarts 等分
文章浏览阅读225次。https://www.cnblogs.com/monjeo/p/9330464.html_jemeter官网下载
文章浏览阅读711次。前言学院第一至第四期具有求职意向的同学中,目前已经有80%的同学拿到了国内外名企的AI算法岗位offer,或者国外名校的AI 硕士、全奖博士录取 offer。在大家的认可下,我们开始了..._cv和nlp哪个好找工作
文章浏览阅读1.1w次,点赞14次,收藏117次。需求分析对于小型局域网中, 对于接入设备的需求,需要在局域网中部署无线网络,通过无线控制器AC管理网络中所有的无线AP设备,下发无线配置信息。无线网络发布2 4G和5G信号,满足不同设备的连接使用。拓扑图:注意:防火墙使用USG5500 ,路由器使用AR2220 , AC使用AC6005 , AP使用AP2050。拓扑描述:防火墙连接外网地址为192.168.12.9/24,路由..._ensp无线2个信号访问同一个地址
文章浏览阅读534次。摘要:笔者根据计算机图形图像处理课程与中职学生学习的特点,分析了目前中职学校计算图形图像处理课程教学中存在的问题,针对如何提高中职学生对计算机图形图像处理课程的学习兴趣和解决实际问题的能力,提出了案例教学法在该课程中的具体实施办法,并对其实践进行了进一步的讨论。关键词:计算机图形图像处理;案例教学;中职当今世界电子商务发展迅速,计算机平面设计这门技术在很多领域都得到广泛应用。《Photoshop图..._图像分类在教育中的应用
文章浏览阅读921次。在网上看了很多博客,终于找到了符合自己智商可理解的打包资源文件方法,现引用如下https://www.cnblogs.com/darcymei/p/9397173.htmlhttps://blog.csdn.net/sinat_27382047/article/details/81304065"""终于把资源文件加载进去了,就是当exe文件移植后,它运行的时候会产生一个临时文件夹,把资源文件存储到..._pyinstaller如何将_internal添加进exe
文章浏览阅读3.3k次,点赞4次,收藏7次。Valn 11组网需求• 交换机GE_2上的VLAN 5 和VLAN 10 为Primary VLAN,其上层端口GigabitEthernet1/0/1需要允许VLAN 5 和VLAN 10 的报文携带VLAN Tag 通过。• 交换机GE_2 的下行端口GigabitEthernet1/0/2 允许Secondary VLAN 2 通过,GigabitEthernet1/0/3 允许Sec..._新华3模拟器vlan配置
文章浏览阅读4.7w次,点赞8次,收藏15次。我有一个模板,想按常规做一个div里面放置一个img图片,并且让图片铺满容器,自适应容器大小。HTML结构代码如下(在这个盒模型上,我已经放置了一些不重要的样式)。div style="height:270px;width:400px;border:2px black solid;"> a href="http://www.paipk.com">img src="..." alt="拍_img 铺满
文章浏览阅读947次。UnicodeDecodeError: 'gbk' codec can't decode byte 0xfa in position 4669: illegal multibyte sequenceUnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 5: invalid start bytewith open('进线汇总20201211.csv',encoding='utf8') as f: t = f._ebpf bcc unicodedecodeerror: 'utf-8' codec can't decode byte 0xb0 in positio
文章浏览阅读1.3k次。使用条件序列GAN改进NMT原文《Improving Neural Machine Translation with Conditional Sequence Generative Adversarial Nets》课程作业,因为要导出pdf所以粘贴到CSDN了,34章是笔者翻译的部分。当一篇post吧,求别喷,有问题请留言我一定改,一定改。摘要本文提出了一种将GANs应用于NMT领域的方..._improving neural machine translation with conditional sequence generative ad