Funpack2-5 基于 ESP32-S3-BOX-LITE的语音播报系统实现
本片文章分享我在Funpack第二季第五期活动中使用 ESP32-S3-BOX-LITE 实现一个语音播报系统的流程及效果展示
标签
嵌入式系统
Funpack活动
开发板
反正都一样
更新2023-08-04
427

FviaEHysOP3HqiNqQb_FJWTpd20A

项目介绍

这里是我参加Funpack第二季第五期活动的任务总结报告,我所完成的是任务一,使用ESP32的WiFi和TTS功能,实现一个语音播报系统。

项目的大概思路如下:

  1. ESP32联网:使用ESP32的Wi-Fi功能与无线网络进行连接。可以通过配置您的无线网络的SSID和密码,使ESP32能够连接到本地网络。

  2. 建立TCP/IP连接:使用ESP32的TCP/IP功能,通过与心知网站的服务器建立连接,发送请求并接收响应数据。需要使用心知网站提供的API接口来获取实时的天气数据。通过向心知网站服务器发送HTTP请求,可以获取天气预报、温度、湿度等相关信息。

  3. 解析和处理数据:需要解析心知网站服务器返回的数据,提取您所需的天气信息,这里就要使用官方的cjson组件。根据需求,可以提取温度、湿度、天气状况等数据并存储在ESP32的变量中,以便后续处理和使用。

  4. TTS语音合成:ESP32官方提供了TTS语音合成转换模型,这使得将文本数据转换为音频数据变得相对简单。可以使用该模型将您获取的天气信息转换为可播放的音频文件。

  5. 音频播放:为了播放音频,需要使用ESP32上的音频芯片。将转换的音频文件加载到ESP32上,并通过与音频芯片的通信,将音频数据发送到芯片进行解码和播放。选择与音频芯片相匹配的接口(例如I2S)来与芯片进行通信,并配置正确的引脚连接。

 

主要硬件介绍

ESP-BOX 是乐鑫发布的新一代 AIoT 开发平台,ESP32-S3-BOX-Lite 开发套件配备了一块 2.4 寸 LCD 显示屏、双麦克风、一个扬声器、两个用于硬件拓展的 Pmod™ 兼容接口和3个独立按键,可构建多样的 HMI 人机交互应用。开发板可实现离线语音唤醒和命令词识别,支持乐鑫自研的高性能声学前端算法构建语音交互系统。开发者可利用开源的 SDK轻松构建在线离线语音助手、智能语音设备、HMI 人机交互设备、多协议网关等多样的应用。

FpgyU1DR3ObkRJ4316wZq5b3FAs4

搭载 ESP32-S3 芯片的 BOX 系列开发板借助语音助手 + 屏幕控制、传感器、红外控制器和智能 Wi-Fi 网关等功能,为用户提供了一个开发智能家居设备控制系统的平台。

 

主要软件介绍

FhRhQbcARF77aFQ8uwb4ibnCtXx7

在软件开发上,我使用的是官方的esp-idf方式进因为官方为我们提供了行开发,原因是丰富的示例程序,涵盖了对esp32开发板上各种外设的使用,并添加了一些流行的第三方组件,如ADC按键检测和lvgl(GUI库)等。这为我们的二次开发提供了极大的便利,无需从底层开始实现,而是可以直接构建在这些示例程序的基础上进行开发。

通过使用官方示例程序包括GPIO、SPI、I2C、UART、ADC等。通过借鉴示例代码,我们可以了解如何正确地使用这些外设,并且直接在我们的项目中应用这些功能,而无需自己从头编写底层驱动程序。同时官方示例程序还集成了许多流行的第三方组件和库,如ADC按键检测和lvgl等。这些组件已经经过官方认证和测试,确保与esp-idf的API兼容,并且容易集成到我们的项目中。这样,我们无需自己去查找、集成和调试这些组件,而是直接使用官方提供的示例程序和文档。再而通过借助官方示例程序和集成的组件,我们可以更加高效地进行开发。我们可以利用这些示例程序提供的功能和代码结构,快速构建原型和功能验证。同时,这也帮助我们避免一些常见的错误和问题,使开发过程更加顺利和快速。

    /* Initialize NVS. */
    err = nvs_flash_init();

    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK( err );

    ESP_ERROR_CHECK(bsp_board_init());
    ESP_ERROR_CHECK(bsp_board_power_ctrl(POWER_MODULE_AUDIO, true));
    es8156_codec_set_voice_volume(100);

    xTaskCreatePinnedToCore(key_task,"key_task",1024,"key_task",2,NULL,tskNO_AFFINITY);

首先,则是各种初始化的流程,包括NVS初始化,NVS(Non-Volatile Storage)是ESP32上的一种非易失性存储器,被用于存储关键的配置和状态信息。在初始化过程中,我们需要调用官方提供的API来初始化NVS,并设置各种初始化参数,如分区表和存储区大小等。

调用官方的例程板子初始化,如果我们需要在开发板上使用音频功能,我们需要调用相应的API来启动音频电源,在音频功能启动后,我们可以使用官方提供的API来调整音量大小。对于使用ADC按键的开发板,我们需要创建一个任务来持续检测ADC引脚的状态,以判断按钮是否被按下。我们可以使用官方提供的ADC API来读取ADC引脚的值,并根据设定的阈值来判断按钮的按下状态。通过创建一个按键任务,我们可以实现对按钮事件的响应,并在按下按钮时执行相应的操作或处理。

void key_task(void *data_t){
    adc_calibration_init();

    //ADC1 config
    ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_DEFAULT));
    ESP_ERROR_CHECK(adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11));

    while(1){
        uint16_t adcval=adc1_get_raw(ADC1_CHANNEL_0)/100;
        if(adcval != no_key && adcval != key_state){
            ESP_LOGI(TAG,"adc value key :%d",adcval);
            key_state=adcval;
        }
        vTaskDelay(50/portTICK_PERIOD_MS);
    }
}

任务中也是对adc引脚进行了初始化处理

void net_connect(){
    ESP_ERROR_CHECK(esp_netif_init());

    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        NULL));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = "*****",
            .password = "******",
            /* Setting a password implies station will connect to all security modes including WEP/WPA.
             * However these modes are deprecated and not advisable to be used. Incase your Access point
             * doesn't support WPA2, these mode can be enabled by commenting below line */
	     .threshold.authmode = WIFI_AUTH_WPA2_PSK,

            .pmf_cfg = {
                .capable = true,
                .required = false
            },
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );
}

接下来就是联网处理,在开始联网处理之前,我们需要在项目中导入官方提供的网络库。这些库提供了与WiFi和网络协议相关的功能和API,例如WiFi连接、TCP/IP通信等。官方提供了一套API和示例程序,用于配置和连接ESP32到WiFi网络。在联网处理过程中,我们需要指定要连接的WiFi网络名称(SSID)和对应的密码。为了处理WiFi连接状态变化和其他网络事件,我们可以使用官方提供的事件系统。通过注册事件回调函数,我们可以在不同的事件发生时执行相应的操作。例如,我们可以注册一个连接成功的回调函数,当ESP32成功连接到WiFi网络时调用,以执行后续的任务或逻辑操作。同样地,我们可以注册一系列的事件回调函数来处理不同的网络事件,如断开连接、连接超时等。一旦我们完成了上述配置和回调函数的注册,我们可以调用官方提供的API来启动联网处理。这将使ESP32开始执行连接WiFi和处理网络事件的操作。在处理过程中,我们可以根据需要执行其他操作,如更新数据、发送请求或接收数据等。

static void obtain_time(void)
{
    initialize_sntp();

    // wait for time to be set
    time_t now = 0;
    struct tm timeinfo = { 0 };
    int retry = 0;
    const int retry_count = 10;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);

    char strftime_buf[64];

    // Set timezone to China Standard Time
    setenv("TZ", "CST-8", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
}

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        esp_wifi_connect();
        ESP_LOGI(TAG,"retry connect to the AP");
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        obtain_time();
    }
}

在事件的函数中,成功连上网络的话则先进行的是ntp校准设备时间。

    const esp_partition_t* part=esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "voice_data");
    if (part==NULL) { 
        ESP_LOGI(TAG,"Couldn't find voice data partition!\n"); 
        return 0;
    } else {
        ESP_LOGI(TAG,"voice_data paration size:%d\n", part->size);
    }
    void* voicedata;
    spi_flash_mmap_handle_t mmap;
    err=esp_partition_mmap(part, 0, part->size, SPI_FLASH_MMAP_DATA, &voicedata, &mmap);
    if (err != ESP_OK) {
        ESP_LOGI(TAG,"Couldn't map voice data partition!\n"); 
        return 0;
    }
    esp_tts_voice_t *voice=esp_tts_voice_set_init(&esp_tts_voice_template, (int16_t*)voicedata); 

    esp_tts_handle_t *tts_handle = esp_tts_create(voice);              // 创建tts对象

再之后就是参考官方的TTS语音合成例程,首先,我们需要在项目中导入官方提供的TTS库。这个库通常包含了语音合成引擎所需的算法和模型,用于将文本转换为语音。在进行语音合成之前,我们需要设置一些参数以控制合成结果的质量和效果。这些参数可以包括语速、音调、音量等。我们可以使用官方提供的API来设置这些参数。

一旦设置完语音合成参数,我们可以调用官方提供的API来初始化语音合成引擎。这将加载所需的模型和算法,并准备好进行后续的语音合成操作。在语音合成引擎初始化完成后,我们可以使用官方提供的API将文本转换为语音。我们需要传入要合成的文本和相应的参数,通过调用合成API,语音合成引擎将根据参数生成相应的语音数据。

一旦语音合成完成,我们可以使用官方提供的API将合成的语音数据发送给音频输出设备进行播放。

Fh2IxZ5MghLabyKw0efekkWpmIcB

char* get_weather(void)
{
    int8_t return_res = 1;
    char* city_buffer = NULL;
    int content_length = 0;

    esp_http_client_config_t config = 
    {
        .event_handler = _http_event_handler,
        .url = WEB_PATH,
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    if( client == NULL )
    {
        return NULL;        
    }

    // GET Request
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    esp_err_t err = esp_http_client_open(client, 0);
    if (return_res && err != ESP_OK) 
    {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
        return_res = 0;
    } 
    else 
    {
        content_length = esp_http_client_fetch_headers(client);
        if (content_length < 0) 
        {
            ESP_LOGE(TAG, "HTTP client fetch headers failed");
            return_res = 0;
        } else 
        {
            city_buffer = malloc(300);
            memset(city_buffer, 0, 300);
            if(city_buffer == NULL)
            {
                return_res = 0;
            }
            else
            {
                int data_read = esp_http_client_read_response(client, city_buffer, 300);
                if (data_read >= 0) 
                {
                    ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d, data_read = %d",
                    esp_http_client_get_status_code(client),
                    esp_http_client_get_content_length(client),
                    data_read);
                    //ESP_LOG_BUFFER_HEX(TAG, city_buffer, data_read);
                    ESP_LOGI( TAG, "Data %s \r\n",  city_buffer);
                }
                else 
                {
                    return_res = 0;
                    ESP_LOGE(TAG, "Failed to read response");
                }
            }
        }
    }
    esp_http_client_close(client);

    if( return_res == 0 )
    {
        free(city_buffer);
        city_buffer = NULL;
    }

    return city_buffer;
}
static bool parse_weather_json(char *analysis_buf)
{
    uint16_t year,month,day,hour,min;

    if( analysis_buf == NULL )
    {
        return false;
    }

    cJSON * json_root = cJSON_Parse(analysis_buf);
    if( json_root != NULL )
    {
        cJSON *cjson_arr = cJSON_GetObjectItem(json_root,"results");
        cJSON *cjson_location = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0),"location");
        ESP_LOGI(TAG, "城市 -> %s", cJSON_GetObjectItem(cjson_location,"name")->valuestring );
        cJSON *cjson_now = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0),"now");
        ESP_LOGI(TAG, "天气 -> %s", cJSON_GetObjectItem(cjson_now,"text")->valuestring );
        ESP_LOGI(TAG, "code -> %s", cJSON_GetObjectItem(cjson_now,"code")->valuestring );
        ESP_LOGI(TAG, "气温 -> %s", cJSON_GetObjectItem(cjson_now,"temperature")->valuestring );

        sprintf(voice_weather,"当前城市为%s,天气为%s,温度为%s摄氏度",cJSON_GetObjectItem(cjson_location,"name")->valuestring,cJSON_GetObjectItem(cjson_now,"text")->valuestring,cJSON_GetObjectItem(cjson_now,"temperature")->valuestring);
        
        cJSON_Delete(json_root);
    }

    free(analysis_buf);

    return true;
}

之后按下右键后,则会使用官方提供的网络库来建立与心知天气网的连接。这需要我们指定心知天气网的API地址以及其他必要的信息,如API密钥、城市编码等。通过调用合适的API,我们可以建立与心知天气网的连接,向其发送请求并获取响应。一旦连接建立成功,我们可以发送HTTP请求来获取天气数据。根据心知天气网的API文档,我们可以使用合适的API来请求实时的天气数据。心知天气网将以JSON格式返回响应,其中包含有关天气的各种信息,如温度、湿度、风速等。由于返回的天气数据是以JSON格式进行传输的,我们需要使用cJSON组件来解析和提取这些数据。cJSON是一种轻量级的C语言JSON解析器,可以帮助我们将JSON数据转换为可操作的C语言对象。我们可以使用cJSON提供的API来解析JSON响应,并提取其中我们所需的天气信息。根据我们想要获取的天气信息,我们可以遍历解析后的JSON对象,并提取其中的特定字段和值。这可以包括当前温度、天气状况、降雨概率等。通过使用cJSON提供的API,我们可以方便地获取并存储这些信息。最后则是合成为一个字符串。

                ESP_LOGI(TAG,"左键");
                if(voice_weather[0] != 0){
                    if(esp_tts_parse_chinese(tts_handle,voice_weather)){
                        int len[1]={0};
                        do{
                            short *pcm_data = esp_tts_stream_play(tts_handle,len,0);
                            i2s_write(I2S_NUM_0, (const char *)pcm_data, len[0]*2 ,&bytes_write ,portMAX_DELAY);
    //                        esp_audio_play(pcm_data,len[0]*2 ,portMAX_DELAY);
                        }while(len[0] > 0);
                    }
                    memset(voice_weather,0,sizeof(voice_weather));
    //                esp_tts_stream_reset(tts_handle);                    
                }

当检测到按下左键时,就会调用tts的相应api函数,将上面得到的字符串数据转换成音频数据,并通过I2S的方式音频数据传输给音频芯片进行播放天气情况。

总结

在本次活动中,学习了如何利用esp官方的TTS语音合成模型库。在过程中遇到的问题,通过百度搜索都能找到适合的答案,使自我得到了提升感谢硬禾学堂平台。

附件下载
factory_demo.zip
源码
团队介绍
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号