Funpack2-5 用ESP32-S3-BOX-LITE的tts功能实现的语音播报系统
Funpack2-5 ESP32-S3-BOX-LITE esp-idf 天气预报 语音 tts 网页抓取 json 自动校时 wifi
标签
嵌入式系统
Funpack活动
测试
aramy
更新2023-08-01
2208

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

任务选择:拿到板子后,很开心的了解到ESP32-S3支持esp-idf、arduino、micropython编程。FlW4d_a99Qt8Ly9RnWdqw_cn5DQy从官方提供的机构简图可以看见,这个板子上的传感器并不多,但是对语音支持特别好!有扬声器+双麦克风,板子的主打应该就是语音方面的AI。自己对三种编程语言分别尝试了一下,很可惜,板子太新了,语音识别和语音合成功能在arduino、micropython都没能尝试成功。所以这次项目只能选用最不熟悉的esp-idf了。项目选择就选择了任务一:使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。

任务实现:FnyG9_N4r_SoprI3-uAZ0NbTb2Va

定下了任务,需要使用到的外设有按键、扬声器、wifi、tts。接下来就是逐一搞定各种外设的驱动和使用。在micropython上尝试驱动I2S,但是没能成功,声音放弃了。在Arduino上语音转换tts部分只能通过联网处理,再加上扬声器驱动也有问题,所以最后只好选择esp-idf来进行开发了。

首先是esp-idf的安装,这块网络上的详细步骤挺多的,按照网络上的步骤,一步步地操作,就能完成。这里我使用的是win10+vscode+esp-idf4.4.4。
不得不说乐鑫官方提供的esp-idf功能超级强大,例程也是相当的丰富。不过,也正因为esp-idf的功能强大,感觉很难掌握!针对ESP32-S3-BOX-Lite,官方提供了例程。这里因为自己搭建的esp-idf环境是4.4的所以需要切换版本进行下载。

git clone -b v0.3.0 --recursive https://github.com/espressif/esp-box.git

官方的例程提供了wifi联网、TFT屏幕的Lvgl的显示、语音命令的识别、音频的播放等功能。经过几天艰难的努力,终于成功编译完成了官方例程。所以这时的思路,就是使用官方的例程,消减其功能模块,以实现自己项目的需求。但是尝试解读esp-box的代码,发现自己基本无法读懂(*>﹏<*)。努力了几次,各种报错,实在是不熟悉,不知道如何解决,只能放弃了。

然后在乐鑫的官网又找到一个tts的例程,拉下来编译到时很顺利,可是烧录到开发板后,板子不工作。经过仔细查看,发现这个例程只是支持了ESP32-S3-BOX。而ESP32-S3-BOX的扬声器驱动部分和ESP32-S3-BOX-Lite的驱动部分并不一样,导致这个例程在Lite上并不适用。经过群里的老师帮助,修改了esp-skainet的例程,使其能够驱动起ES856的扬声器。有了这个能正常使用的例程,就改变完成项目的思路,选择esp-skainet中的chinese_tts例子,在例子中添加对应的功能,来完成项目。

首先添加wifi功能,想获取天气预报、B站粉丝数量,都需要互联网功能。这里套用了esp-idf例程中的http_request的联网和获取网页的例子。来连接wifi,wifi的sid和密码在sdkconfig中进行配置即可。

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();
    }
    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();
        // parse_weather_json(xinzhi_task(parse_city_json(city_task())));
        // xinzhi_task();
    }
}

/* Initialize Wi-Fi as sta and set scan method */
static void fast_scan(void)
{
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    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));

    // Initialize default station as network interface instance (esp-netif)
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif);

    // Initialize and start WiFi
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = CONFIG_EXAMPLE_WIFI_SSID,
            .password = CONFIG_EXAMPLE_WIFI_PASSWORD,
            // .scan_method = DEFAULT_SCAN_METHOD,
            // .sort_method = DEFAULT_SORT_METHOD,
            .threshold.rssi = CONFIG_EXAMPLE_WIFI_SCAN_RSSI_THRESHOLD,
            // .threshold.authmode = DEFAULT_AUTHMODE,
        },
    };
    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联网,联网成功后就自动去和互联网校正时间,并将获得的时间写到esp32s3里去。之后需要使用时间,就可以直接从esp32s3里获取了。

static void initialize_sntp(void)
{
    ESP_LOGI(TAG, "Initializing SNTP");
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    // sntp_set_time_sync_notification_cb(time_sync_notification_cb);
    sntp_init();
}
static void obtain_time(void)
{
    /**
     * NTP server address could be aquired via DHCP,
     * see LWIP_DHCP_GET_NTP_SRV menuconfig option
     */
#ifdef LWIP_DHCP_GET_NTP_SRV
    sntp_servermode_dhcp(1);
#endif
    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 Eastern Standard Time and print local time
    setenv("TZ", "EST5EDT,M3.2.0/2,M11.1.0", 1);
    tzset();
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
    ESP_LOGI(TAG, "The current date/time in New York is: %s", strftime_buf);

    // 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);
}

 

还需要获取天气预报和B站粉丝数量。天气预报使用心知天气,获取当天和明天两天的天气情况。通过get方法访问web页面,获得的内容是一串json字符串。然后对json字符串进行解析。

esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
    switch (evt->event_id)
    {
    case HTTP_EVENT_ERROR:
        ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
        break;
    case HTTP_EVENT_ON_CONNECTED:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
        break;
    case HTTP_EVENT_HEADER_SENT:
        ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
        break;
    case HTTP_EVENT_ON_HEADER:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
        break;
    case HTTP_EVENT_ON_DATA:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
        break;
    case HTTP_EVENT_ON_FINISH:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
        break;
    case HTTP_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
        break;
    }
    return ESP_OK;
}

// 获取心知天气预报 返回json字符串
char *webget_task(char *weburl)
{
    int8_t return_res = 1;
    char *weather_buffer = NULL;
    int content_length = 0;
    esp_http_client_config_t config =
        {
            .event_handler = _http_event_handler,
            .url = weburl,
        };
    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
        {
            weather_buffer = malloc(content_length + 1);
            memset(weather_buffer, 0, content_length + 1);
            if (weather_buffer == NULL)
            {
                return_res = 0;
            }
            else
            {
                int data_read = esp_http_client_read_response(client, weather_buffer, content_length);
                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, weather_buffer, data_read);
                    ESP_LOGI(TAG, "Data %s \r\n", weather_buffer);
                }
                else
                {
                    ESP_LOGE(TAG, "Failed to read response");
                    return_res = 0;
                }
            }
        }
    }
    esp_http_client_close(client);

    if (!return_res)
    {
        free(weather_buffer);
        weather_buffer = NULL;
    }
    return weather_buffer;
}

//将天气预报的信息,构建需要播报的语音字符串
static bool parse_weather_json(char *analysis_buf, char *tts_str)
{
    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);
        sprintf(tts_str, "%s%s", tts_str, cJSON_GetObjectItem(cjson_location, "name")->valuestring);
        cJSON *cjson_days = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "daily");
        cJSON *cjson_today = cJSON_GetArrayItem(cjson_days, 0);
        ESP_LOGI(TAG, "今天天气 -> %s", cJSON_GetObjectItem(cjson_today, "text_day")->valuestring);
        ESP_LOGI(TAG, "最高温度 -> %s", cJSON_GetObjectItem(cjson_today, "high")->valuestring);
        ESP_LOGI(TAG, "最低温度 -> %s", cJSON_GetObjectItem(cjson_today, "low")->valuestring);
        sprintf(tts_str, "%s今天天气%s;温度%s到%s摄氏度;湿度:百分之%s。", tts_str,
                cJSON_GetObjectItem(cjson_today, "text_day")->valuestring,
                cJSON_GetObjectItem(cjson_today, "low")->valuestring,
                cJSON_GetObjectItem(cjson_today, "high")->valuestring,
                cJSON_GetObjectItem(cjson_today, "humidity")->valuestring);
        cJSON *cjson_tomorrow = cJSON_GetArrayItem(cjson_days, 0);
        ESP_LOGI(TAG, "明天天气 -> %s", cJSON_GetObjectItem(cjson_tomorrow, "text_day")->valuestring);
        ESP_LOGI(TAG, "最高温度 -> %s", cJSON_GetObjectItem(cjson_tomorrow, "high")->valuestring);
        ESP_LOGI(TAG, "最低温度 -> %s", cJSON_GetObjectItem(cjson_tomorrow, "low")->valuestring);
        sprintf(tts_str, "%s明天天气%s;温度%s到%s摄氏度;湿度:百分之%s。", tts_str,
                cJSON_GetObjectItem(cjson_today, "text_day")->valuestring,
                cJSON_GetObjectItem(cjson_today, "low")->valuestring,
                cJSON_GetObjectItem(cjson_today, "high")->valuestring,
                cJSON_GetObjectItem(cjson_today, "humidity")->valuestring);
        ESP_LOGI(TAG, "%s", tts_str);
        cJSON_Delete(json_root);
    }

    free(analysis_buf);
    return true;
}
//将bilibili的信息,构建需要播报的语音字符串  {"code":0,"message":"0","ttl":1,"data":{"mid":596836443,"following":53,"whisper":0,"black":0,"follower":28}}
static bool parse_bilibili_json(char *analysis_buf, char *tts_str)
{
    if (analysis_buf == NULL)
        return false;
    cJSON *json_root = cJSON_Parse(analysis_buf);
    if (json_root != NULL)
    {
        cJSON *cjson_data = cJSON_GetObjectItem(json_root, "data");
        ESP_LOGI(TAG, "粉丝数 -> %d", cJSON_GetObjectItem(cjson_data, "follower")->valueint);
        sprintf(tts_str, "%s当前粉丝数量%d位。", tts_str, cJSON_GetObjectItem(cjson_data, "follower")->valueint);
        ESP_LOGI(TAG, "%s", tts_str);
        cJSON_Delete(json_root);
    }

    free(analysis_buf);
    return true;
}

//获得当前时间,并形成字符串
static void parse_currtime(char *tts_str)
{
    time_t now_t;
    struct tm *area;
    setenv("TZ", "CST-8", 1);
    tzset();
    now_t = time(NULL);
    area = localtime(&now_t);
    // printf("Local time is: %s", asctime(area));
    strftime(tts_str, 100, "现在时间是%H点%M分。", area);
    ESP_LOGI(TAG, "%s", tts_str);
    // printf("The number of seconds since January 1, 1970 is %ld\r\n", now_t);
}

这里有两个问题需要格外注意。问题1:在添加wifi模块后,main函数引入了wifi相关的头文件,然后编译就总是报错,说找不到对应的头文件,这时需要修改main函数所在文件同级目录下的CMakeLists.txt文件,在编译文件时添加上对应的模块。
FpMilkMGEKjO6n4FwfU2zuB7g95d

set(srcs
    main.c
    )

set(include_dirs 
    include
    )

set(requires
    esp-sr
    hardware_driver
    sr_ringbuf
    player
    )

idf_component_register(SRCS ${srcs}
                       INCLUDE_DIRS ${include_dirs} "." 
                       REQUIRES ${requires}
                       PRIV_REQUIRES nvs_flash esp_http_client esp_adc_cal )

add_definitions(-w)

问题2:这里获取心知天气和B站信息,都是走的https协议,这个协议需要证书,esp-idf提供的默认证书,貌似对心知天气网站可以用,但是B站就会报错。解决方案有两个,一个是提供相应的证书。一个是忽略证书。我这里使用后一个方法,忽略证书。FnQFP2hfeSzQcZjPVvGtF2i2weY1
获得了需要的字符串(天气、时间、B站粉丝数),剩下的就是交给esp-idf提供的tts方法来生成语音了,生成语音后,交给扬声器进行播报!

            tts_flag=false;
            if (esp_tts_parse_chinese(tts_handle, tts_str)) // 文字解析成拼音
            {
                int len[1] = {0};
                do
                {
                    short *pcm_data = esp_tts_stream_play(tts_handle, len, 2); // 拼音转换成pcm音频
                    esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);       //播放音频
                } while (len[0] > 0);
            }
            esp_tts_stream_reset(tts_handle); // 重置 tts 流并清除 TTS 实例的所有缓存

再添加一个键盘选择,通过三个按键选择是播放天气预报、时间或B站粉丝数量。这里的按键是使用ADC方式获取键盘的输入的,所以通过获得的AD值,来判断输入。

// ADC初始化
u8_t key = 4; //按键值 0 右键  2 中间键   3 左键   4 无按键
static esp_adc_cal_characteristics_t adc1_chars;
#define ADC1_EXAMPLE_CHAN0 ADC1_CHANNEL_0 //使用GPOI1作为AD按键的输入
static bool adc_calibration_init(void)
{
    esp_err_t ret;
    bool cali_enable = false;
    ret = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP_FIT);
    if (ret == ESP_ERR_NOT_SUPPORTED)
    {
        ESP_LOGW(TAG, "Calibration scheme not supported, skip software calibration");
    }
    else if (ret == ESP_ERR_INVALID_VERSION)
    {
        ESP_LOGW(TAG, "eFuse not burnt, skip software calibration");
    }
    else if (ret == ESP_OK)
    {
        cali_enable = true;
        esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_DEFAULT, 0, &adc1_chars);
    }
    else
    {
        ESP_LOGE(TAG, "Invalid arg");
    }
    return cali_enable;
}

void listen_key_task(void *pvPar)
{
    adc_calibration_init(); //初始化ADC
    // ADC1 config
    ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_DEFAULT));
    ESP_ERROR_CHECK(adc1_config_channel_atten(ADC1_EXAMPLE_CHAN0, ADC_ATTEN_DB_11));
    while (1)
    {
        u16_t adcval = adc1_get_raw(ADC1_EXAMPLE_CHAN0) / 1000;

        if (adcval != 4 && adcval != key)
        {
            ESP_LOGI(TAG, "Adc value key:%d", adcval);
            key = adcval;
        }
        // printf("key %d\r\n", );
        //使用此延时API可以将任务转入阻塞态,期间CPU继续运行其它任务
        vTaskDelay(80 / portTICK_PERIOD_MS);
    }
}

FvTjnoeUib_5MGCcnLGdXIaE7QPr

最后就是屏幕了,想添加个lvgl的功能,尝试从esp-box的例程中剥离出lvgl部分,尝试剥离了几次,都失败了,感觉例程中模块耦合度有些高,各种报错,实在无力解决。最终放弃了屏幕显示的功能。

心得体会:感谢funpack带来的这期活动。接触到了如此优秀的板卡,如此小巧的板子上,能实现离线的语音命令字的识别,语音合成功能。esp-idf功能也是超级强大,就是过于复杂,感觉上手困难,期待更多的学习机会,能够掌握esp-idf的强大功能。

代码大小超出网站限制:链接:https://pan.baidu.com/s/1ytPenMRSA3bJKbEj-c4KQA  提取码:8888

 

团队介绍
折腾小能手
团队成员
aramy
单片机业余爱好者,瞎捣鼓小能手。
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号