基于ESP32-S3-BOX-Lite的语音合成与播报系统(esp-idf+WiFi+HTTPS+TTS)_esp32 tts语音-程序员宅基地

技术标签: 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,支持用户配置数据缓存与指令缓存。

  1. Xtensa 32 位 LX7 双核处理器,主频高达 240 MHz
  2. 内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM
  3. 额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions)
  4. 45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC 主机控制器和 TWAITM 控制器等
  5. 基于 AES-XTS 算法的 Flash 加密和基于 RSA 算法的安全启动,数字签名和 HMAC 模块,“世界控制器 (World Controller)”模块
    ESP32-S3-BOX-Lite硬件总览

项目设计

开发环境及工程目录

强烈建议采用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));

WiFi

参考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

参考 esp-idf文档:分区表

HTTPS请求

该部分完成对 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字段的 datafollower 是想解析得到的数据。由于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);
    ...
}

TTS语音合成与播报

cJSON解析

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() 释放指针。

TTS初始化

经过json解析后得到的粉丝数数据,交由TTS模块完成语音合成。首先需要为板子进行TTS功能的初始化。

参考esp-skainet所提供的有关TTS的示例:chinese_tts

  1. 存放voice_data数据,esp-box仓库提供了现成的 voice_dataesp_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 路径,其余可照抄。

  1. 将示例程序中的部分代码借鉴下来:
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_PREVBOARD_BTN_ID_ENTERBOARD_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);
}

LVGL数据可视化显示

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请求并返回如下图所示数据:

HTTPS请求返回数据
之后,按下正面左键,开发板将合成语音并播报,如下图所示,“粉丝数六十九”:

TTS语音合成
同时,屏幕将更新显示:

屏幕显示粉丝数
详细展示参见: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。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_46422143/article/details/131744806

智能推荐

Windows CMD命令大全-程序员宅基地

文章浏览阅读79次。打开cmd快捷键 window键+RCMD命令锦集 1. gpedit.msc-----组策略  2. sndrec32-------录音机  3. Nslookup-------IP地址侦测器 ,是一个 监测网络中 DNS 服务器是否能正确实现域名解析的命令行工具。 它在 Windows NT/2000/XP 中均可使用 , 但在 Wi...

linux 安装dwm界面,ArchLinux dwm的安装和配置-程序员宅基地

文章浏览阅读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

Echarts 计算每份占比(等分)_echarts 等分-程序员宅基地

文章浏览阅读814次。option = { series: [ { type: "gauge", startAngle: 190, endAngle: -10, min: 0, max: 100, axisLine: { lineStyle: { width: 15, color: [ [0.3, "#91BB7E".._echarts 等分

Jemeter_jemeter官网下载-程序员宅基地

文章浏览阅读225次。https://www.cnblogs.com/monjeo/p/9330464.html_jemeter官网下载

从工作的角度看 CV/NLP/推荐系统选哪个?-程序员宅基地

文章浏览阅读711次。前言学院第一至第四期具有求职意向的同学中,目前已经有80%的同学拿到了国内外名企的AI算法岗位offer,或者国外名校的AI 硕士、全奖博士录取 offer。在大家的认可下,我们开始了..._cv和nlp哪个好找工作

eNSP网络构建—配置无线网络_ensp无线2个信号访问同一个地址-程序员宅基地

文章浏览阅读1.1w次,点赞14次,收藏117次。需求分析对于小型局域网中, 对于接入设备的需求,需要在局域网中部署无线网络,通过无线控制器AC管理网络中所有的无线AP设备,下发无线配置信息。无线网络发布2 4G和5G信号,满足不同设备的连接使用。拓扑图:注意:防火墙使用USG5500 ,路由器使用AR2220 , AC使用AC6005 , AP使用AP2050。拓扑描述:防火墙连接外网地址为192.168.12.9/24,路由..._ensp无线2个信号访问同一个地址

随便推点

计算机图形图像处理在教学中的应用,计算机图形图像处理案例教学法运用-程序员宅基地

文章浏览阅读534次。摘要:笔者根据计算机图形图像处理课程与中职学生学习的特点,分析了目前中职学校计算图形图像处理课程教学中存在的问题,针对如何提高中职学生对计算机图形图像处理课程的学习兴趣和解决实际问题的能力,提出了案例教学法在该课程中的具体实施办法,并对其实践进行了进一步的讨论。关键词:计算机图形图像处理;案例教学;中职当今世界电子商务发展迅速,计算机平面设计这门技术在很多领域都得到广泛应用。《Photoshop图..._图像分类在教育中的应用

python资源文件嵌入exe_pyinstaller将资源文件打包进exe中-程序员宅基地

文章浏览阅读921次。在网上看了很多博客,终于找到了符合自己智商可理解的打包资源文件方法,现引用如下https://www.cnblogs.com/darcymei/p/9397173.htmlhttps://blog.csdn.net/sinat_27382047/article/details/81304065"""终于把资源文件加载进去了,就是当exe文件移植后,它运行的时候会产生一个临时文件夹,把资源文件存储到..._pyinstaller如何将_internal添加进exe

H3C模拟器配置vlan-程序员宅基地

文章浏览阅读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配置

img撑满全屏的方法(img非背景图)_img 铺满-程序员宅基地

文章浏览阅读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 铺满

UnicodeDecodeError: ‘utf-8‘ codec can‘t decode byte 0xb0 in position 5: invalid start byte_ebpf bcc unicodedecodeerror: 'utf-8' codec can't d-程序员宅基地

文章浏览阅读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

使用条件序列GAN改进NMT_improving neural machine translation with conditio-程序员宅基地

文章浏览阅读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