硬件介绍:funpack第二季第5期活动带来的这款ESP32-S3-BOX-Lite AI语音开发套件,是乐鑫打造的一个智能语音设备开发平台。搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。为用户提供了一个基于语音助手、传感器、红外控制器和智能 Wi-Fi 网关等功能开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
板子上集成了两个麦克风、一个扬声器,还有一个2.4寸的屏幕。看官方例程,是一个很厉害的语音方面的开发板。
任务选择:这次的任务选择比较曲折。这个开发板支持micropython、arduino、esp-idf的开发。这里边micropython觉着最简单,于是就选择使用micropython来作为开发工具,去尝试完成任务三:使用板卡的屏幕和联网功能,实现一个在线电子书浏览器,从网络上获取文本并显示在屏幕上,通过按键翻页。
不得不说micropython的效率是真高,很快就搞定了显示、wifi、按键。并且成功搞定了tft下的汉字显示。但是问题也是超级明显。问题1:汉字显示,这里的汉字显示使用的是查表方式,从中文字库中查找汉字。中文字库文件包含了常用的汉字,提供了三个尺寸的规格,字库文件倒也不大,ESP32-S3-BOX-Lite能轻松保存下来。可是查找效率就真的不高,速度很慢,汉字显示时一个一个汉字蹦出来。问题2:网页抓取问题。python有很成熟的html解析库,都很好用。可是在mpy下我没能找到一个能用的html解析库来用。而且小说网页的返回页面,都比较大,mpy处理这样超长的的文本字符串时,感觉内存有问题,经常报错。基于以上两个困难,最终放弃了任务三。
最后选择了任务1:使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。micropython没有找到驱动乐鑫的TTS的方法,扬声器的I2S驱动也没能搞定,最终开发工具也只能放弃简单的mpy,改用ESP-IDF来进行开发。
任务实现:
先做了个流程图,可以看出需要的外设有按键、wifi、扬声器。ESP-IDF环境真的是麻烦,对网络要求很高,单单esp-idf的安装,就搞了几天。不过这块网络上的详细步骤挺多的,按照网络上的步骤,一步步地操作,就能完成。这里我使用的开发工具为vscode。esp-idf做开发还是蛮复杂的,好在乐鑫提供了大量的例程可以做参考。这里在群里老师的指导下去了解了https://github.com/espressif/esp-skainet.git这个例程。这个例程是乐鑫提供的文字转语音的例子。以前接触的文字转语音基本流程都是在互联网上,通过一下AI平台,提供需要转换的文字,然后获取到转换后的语音。而这里是离线转换,也就是说文字转换为对应的语音整个过程都是通过ESP32-S3-BOX-Lite来实现的(👍)。并且转换后的语音听着还挺不错的,没有早期电脑上的那种TTS,一个字一个字地往外蹦的生硬感,语句还算连贯。通过源代码查看,TTS部分是调用了esp-adf下的esp-sr/esp-tts的库文件,这部分乐鑫只提供了函数的定义和实现文件的类库,没有提供源代码。猜测使用了机器学习的技术。
首先驱动按键,这里提供了3个按键供用户使用。从电路图可以看出三个按键都接在GPIO1管脚上,通过电阻做分压,不同按键按下时,1管脚上的电压不同。通过电压来判断按键。这里参考esp-idf例程peripherals\adc\single_read\adc,来读取按键的值,将读取到的值除以1000,正好映射到 0、2、3、4四个整数值,用来标记 右键、中间键、左键 、无按键四个动作。并且启动一个线程,用来监控按键的动作。
// 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);
}
}
学着esp-idf例程中使用wifi的方法,这个非常简单,只需要调用example_connect()这个函数的方法(调用前包含好对应的头文件),就能轻易地连接上在sdkconfig中配置的wifi了。联网后,项目需要当前时间的天气情况,这个就需要从互联网上获得了。这里获取天气情况我使用的是心知天气http://api.seniverse.com/v3/weather/now.json?key=xxxxxxx&location=dongguan&language=zh-Hans&unit=c。从心知天气通过get方法,访问上边的地址,就能拿到一串当前天气的json字符串了。然后使用cJSON这个类库,来解析文件内容。这里需要获取当前城市、天气、温度信息,构建成需要播报的字符串。
{"results":[{"location":{"id":"WS0GHKN5ZP7T","name":"东莞","country":"CN","path":"东莞,东莞,广东,中国","timezone":"Asia/Shanghai","timezone_offset":"+08:00"},"now":{"text":"大雨","code":"15","temperature":"27"},"last_update":"2023-07-02T16:40:16+08:00"}]}
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
static char *http_web_request()
{
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 = "http://api.seniverse.com/v3/weather/now.json?key=lxtscavxia1gwsbd&location=dongguan&language=zh-Hans&unit=c",
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL) //如果URL有误,则返回
{
return NULL;
}
// GET Request
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_err_t err = esp_http_client_open(client, 0);
if (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;
}
//将json字符串,拼接成需要预报的字符串
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", 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);
sprintf(tts_str, "%s:天气%s,", tts_str, 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(tts_str, "%s:气温%s摄氏度.", tts_str, cJSON_GetObjectItem(cjson_now, "temperature")->valuestring);
cJSON_Delete(json_root);
}
free(analysis_buf);
return true;
}
最后在主函数中,监听这按键信息。当收到中间按键按下的消息后,就访问一次心知网页,然后调用esp_tts_parse_chinese方法将构建好的字符串形成语音数据,然后送到扬声器播放。若是左右按键按下,就调用es8156_codec_set_voice_volume方法来设置声音大小。这里声音大小范围从0~100,步长值设置为10,但是音量调到最大,声音依然不够嘹亮。直到最后也没能驱动起屏幕,算是留下了一个遗憾!
int app_main()
{
int vol=50; //音量
char *tts_str[50]; // 需要转换的文字
memset(tts_str, 0, 50);
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES)
{
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
xTaskCreatePinnedToCore(listen_key_task, "listen_key_task", 1024, "listen_key_task", 2, NULL, tskNO_AFFINITY);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect()); //连接wifi
ESP_ERROR_CHECK(esp_board_init(AUDIO_HAL_16K_SAMPLES, 1, 16)); // 初始化codec芯片,配置好采样率、声道数、采样大小
esp_tts_voice_t *voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件,来自libvoice_set_xiaole
esp_tts_handle_t *tts_handle = esp_tts_create(voice); // 创建tts对象
es8156_codec_set_voice_volume(vol);
while (1)
{
if (key == 2)
{
char *weather_json = http_web_request(); //获取当前东莞的天气预报
parse_weather_json(weather_json, tts_str);
ESP_LOGI(TAG, "%s", tts_str);
// free(weather_json);
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); //播放音频
// es8156_codec_set_voice_volume((num++)*10);
} while (len[0] > 0);
}
esp_tts_stream_reset(tts_handle); // 重置 tts 流并清除 TTS 实例的所有缓存
key = 4; //重置按键
}
else if (key == 0) //增大音量
{
vol+=10;
if(vol>100) vol=100;
ESP_LOGI(TAG, "vol value :%d", vol);
es8156_codec_set_voice_volume(vol);
key = 4; //重置按键
}
else if (key == 3) //缩小音量
{
vol-=10;
if(vol<0) vol=0;
ESP_LOGI(TAG, "vol value :%d", vol);
es8156_codec_set_voice_volume(vol);
key = 4; //重置按键
}
vTaskDelay(150 / portTICK_PERIOD_MS);
}
return 0;
}
心得体会: 很开心参加funpack举办的这次活动,在这里感受到了ESP32-S3-BOX-Lite板子功能的强大,也感受到了esp-idf的复杂!任务的完成得到交流群里很多老师的帮助,在此表示非常感谢!期待学习各位老师的作品!
代码过大,无法作为附件上传,顾使用网盘共享。
链接:https://pan.baidu.com/s/1zGIxmaQTcxGTc_46paDz6w
提取码:9271