【立创·实战派ESP32-S3】文档教程
第 15 章 语音唤醒与语音命令
乐鑫 ESP32-S3 最有特色的功能,要属它的 AI 功能:语音识别和图像识别。本章学习语音识别应用,下一章学习图像识别应用。
语音识别应用主要参考 ESP-Skainet,它是乐鑫推出的智能语音助手,它的开源地址为:
github 链接:https://github.com/espressif/esp-skainet/blob/master/README_cn.md gitee 链接:https://gitee.com/EspressifSystems/esp-skainet
语音识别的模型和算法,都位于 esp-sr 组件中。
esp-sr 参考文档:https://docs.espressif.com/projects/esp-sr/zh_CN/latest/esp32s3/getting_started/readme.html
该组件提供了 4 个模块,分别是:声学前端算法 AFE、唤醒词检测模型 WakeNet、命令词识别模型 MultiNet、中文语音合成模型。
本章例程,我们实现语音唤醒和命令词识别,实现语音控制音乐播放器。
15.1 使用例程
把开发板提供的【12-speech_recognition】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
开发板开始运行程序后,液晶屏显示和上一个“MP3 音乐播放器”例程一模一样,可以和上一章例程一样,使用触摸屏控制按键动作,这里在这个基础上增加了语音控制功能。
说出“Hi 乐鑫”唤醒,液晶屏上出现一个 gif 动画,如果在 6 秒之内,它没有检测到语音命令,6 秒到,gif 动画自动消失。
唤醒后,可以使用 7 个命令词控制,分别是:播放音乐、暂停、继续、上一首、下一首、声音大一点、声音小一点。
语音命令和触摸屏可以混用,不会冲突。
替换音乐
例程中,播放的音乐,存储在 spiffs 文件夹中。
版权声明:例程中使用的音乐仅供学习交流使用,版权归 fiftysounds.com 所有。
如果想要播放自己的音乐,可以在这个文件夹中替换。在上一个例程中,让大家注意了两点,一个是存储空间,一个是文件名称。
这里还需要注意一点,语音识别,I2S 的采样率等参数是固定的,如果修改,就无法识别语音,所以在播放音乐的时候,不能像上一个例程那样根据 mp3 音频格式随时修改 I2S 参数。这里播放的音乐,必须和语音识别需要的 I2S 参数相符,才能正常播放。
这里需要的 mp3 音乐,采样率必须是 32000 才能正常播放,采样率小于 32000 的音乐听起来会变“加速”,采样率大于 32000 的音乐听起来会变“慢速”。
语音识别模型需要的音频格式其实是 16000Hz、16bit。这里之所以需要 32000Hz,是因为我们例程中开启了 I2S_TDM 模式,es7210 的 I2S_TDM 模式和 I2S 模式的时序区别如下图所示:
在 I2S_TDM 模式下,有 4 个通道,比 I2S 模式多了 2 个通道,如果格式还是配置为 16000Hz、16bit,实际上是 8000Hz
、8bit。配置为 32000Hz,32bit,实际输入模型的音频即是 16000Hz、16bit。
可以使用 ffmpeg 工具,修改你要播放音乐的采样率。
关于 ffmpeg 工具的安装,可以查看实战派 C3 开发板教程的第 17.2.2 章节。
在 PowerShell 中,输入以下命令,修改 mp3 音乐的采样率,其中,input.mp3 是要修改的 mp3 名称,output.mp3 是修改后的 mp3 文件名称,input 和 output 不能重名。
ffmpeg -i input.mp3 -ar 32000 output.mp3
mp3 音乐一般的采样率是 44100,修改为 32000 的采样率后,存储大小一般会变小,如果还嫌存储空间大,可以修改码率,让体积更小。
下面的命令,修改码率,其中,32k 是要修改的目标码率。
ffmpeg -i input.mp3 -b:a 32k output.mp3
查看 mp3 音乐的采样率、码率等参数,使用下面的命令:
ffprobe input.mp3
如果想要播放 SD 卡中的音乐,和上一章讲的方法一样,初始化 SD 卡后,在 mp3_player_init 函数中,file_iterator_new 函数的参数,换成 SD 卡的路径就可以了。播放 SD 卡的音乐,就可以显示中文名称了。
在实际应用产品中,像开发板这样一个 I2S 接口同时接音频输入和音频输出的情况,如果既需要语音识别,又需要语音播放,只能像上面讲的方法一样,自己制作需要播放的音频文件。这方面的实际应用,例如智能冰箱、智能洗衣机等。
如果需要直接按照 mp3 本来的格式播放,可以使用两个 I2S 接口,一个接音频输入,另外一个接音频输出,两个 I2S 的采样率就可以单独配置了。这种方向,例如智能音箱等。
更换唤醒词
语音唤醒词,只能选 menuconfig 中提供的,如果想要自定义唤醒词,可以和乐鑫官方联系制作。
可选的唤醒词如下:
语音命令词
语音命令词,可以自己随便定义。在文件 app-sr 中通过程序定义就可以,如下所示:
esp_mn_commands_clear(); // 清除当前的命令词列表
esp_mn_commands_add(1, "bo fang yin yue"); // 播放音乐
esp_mn_commands_add(2, "zan ting"); // 暂停
esp_mn_commands_add(3, "ji xu"); // 继续
esp_mn_commands_add(4, "shang yi shou"); // 上一首
esp_mn_commands_add(5, "xia yi shou"); // 下一首
esp_mn_commands_add(6, "sheng yin da yi dian"); // 声音大一点
esp_mn_commands_add(7, "sheng yin xiao yi dian"); // 声音小一点
esp_mn_commands_update(); // 更新命令词
2
3
4
5
6
7
8
9
第 1 条语句清除命令词列表。
中间几条语句,增加命令,命令使用拼音。
最后 1 条语句,更新命令词列表。
识别到语音命令后,会返回命令的 ID 号。
15.2 例程讲解
本例程只是在上一章音乐播放器例程的基础上,加了语音控制命令。
先看主函数,代码如下:
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lvgl_start(); // 初始化液晶屏lvgl接口
bsp_spiffs_mount(); // SPIFFS文件系统初始化
bsp_codec_init(); // 音频初始化
mp3_player_init(); // MP3播放器初始化
app_sr_init(); // 语音识别初始化
}
2
3
4
5
6
7
8
9
10
11
12
和上一章音乐播放器例程的主函数相比,多了最后一行 app_sr_init()
函数,用来初始化语音识别。
app_sr_init()
函数代码如下:
void app_sr_init(void)
{
models = esp_srmodel_init("model"); // 获取模型 名称“model”和分区表中装载模型的名称一致
afe_handle = (esp_afe_sr_iface_t *)&ESP_AFE_SR_HANDLE; // 先配置afe句柄 随后才可以调用afe接口
afe_config_t afe_config = AFE_CONFIG_DEFAULT(); // 配置afe
afe_config.wakenet_model_name = esp_srmodel_filter(models, ESP_WN_PREFIX, NULL); // 配置唤醒模型 必须在create_from_config之前配置
afe_data = afe_handle->create_from_config(&afe_config); // 创建afe_data
ESP_LOGI(TAG, "wakenet:%s", afe_config.wakenet_model_name); // 打印唤醒名称
task_flag = 1;
xTaskCreatePinnedToCore(&detect_Task, "detect", 8 * 1024, (void*)afe_data, 5, NULL, 1);
xTaskCreatePinnedToCore(&feed_Task, "feed", 8 * 1024, (void*)afe_data, 5, NULL, 0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
第 3 行,初始化模型。esp_srmodel_init()函数位于 esp-sr 组件中。
第 5~6 行,配置 AFE 声学前端算法模型。AFE_CONFIG_DEFAULT()把 AFE 配置为默认设置。
第 8~10 行,配置唤醒词模型。
第 13 行,创建检测任务 detect_Task,检测唤醒词和命令词,并执行相应动作。
第 14 行,创建音频输入任务 feed_Task,用来获取 I2S 的数据,并把数据输入给模型。
detect_Task 和 feed_Task 分别工作在 CPU1 和 CPU0 上,并且两个任务都有参数,参数是 afe_data。
feed_Task 任务
void feed_Task(void *arg)
{
esp_afe_sr_data_t *afe_data = arg; // 获取参数
int audio_chunksize = afe_handle->get_feed_chunksize(afe_data); // 获取帧长度
int nch = afe_handle->get_channel_num(afe_data); // 获取声道数
int feed_channel = bsp_get_feed_channel(); // 获取ADC输入通道数
assert(nch <= feed_channel);
int16_t *i2s_buff = heap_caps_malloc(audio_chunksize * sizeof(int16_t) * feed_channel, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM); // 分配获取I2S数据的缓存大小
assert(i2s_buff);
while (task_flag) {
bsp_get_feed_data(false, i2s_buff, audio_chunksize * sizeof(int16_t) * feed_channel); // 获取I2S数据
afe_handle->feed(afe_data, i2s_buff); // 把获取到的I2S数据输入给afe_data
}
if (i2s_buff) {
free(i2s_buff);
i2s_buff = NULL;
}
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
该任务函数的主要任务,就是从麦克风获取声音,然后把声音数据传递给模型。
bsp_get_feed_data()
函数用来获取麦克风采集到的 I2S 数据。
feed()
函数用来把获取的 I2S 数据输入到模型。
bsp_get_feed_data()
函数有 3 个参数,第 1 个参数表示是否采集原始数据,第 2 个参数是需要把数据采集到哪个数组,第 3 个参数是需要采集的 I2S 数据字节大小。这里,我们的第 1 个参数是 false,说明数据需要处理,第 2 个参数表示要把数据采集到 i2s_buff 指向的内存,第 3 个参数是需要采集的字节数,这个字节数,和前面给 i2s_buff 分配的内存大小是一致的。audio_chunksize 是帧长度,默认是 512。feed_channel 是有多少个 ADC 通道,我们这里接了 4 个 MIC,都已开启,所以 feed_channel 是 4。因为获取到的每一个数据都是 16 位的,所以这里还要乘以 int16_t 才是真正的字节数。
下图是需要传输给 adf_data 的 I2S 数据流,其中,每个框代表一个 int16_t 数据。mic_l 表示左声道数据,mic_r 表示右声道数据,ref 表示参考通道数据,需要按照下面顺序存放好数据,然后把数据输入给 afe_data 模型。
现在我们采集到的 I2S 数据,是 4 路 MIC 数据,需要整理一下才可以传入到模型,看下面的 bsp_get_feed_data 函数,这个函数中,用来处理数据。
esp_err_t bsp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len)
{
esp_err_t ret = ESP_OK;
int audio_chunksize = buffer_len / (sizeof(int16_t) * ADC_I2S_CHANNEL);
ret = esp_codec_dev_read(record_dev_handle, (void *)buffer, buffer_len);
if (!is_get_raw_channel) {
for (int i = 0; i < audio_chunksize; i++) {
int16_t ref = buffer[4 * i + 0];
buffer[3 * i + 0] = buffer[4 * i + 1];
buffer[3 * i + 1] = buffer[4 * i + 3];
buffer[3 * i + 2] = ref;
}
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过对 I2S 获取到的数据 buffer 打印验证,I2S 接收到的 4 个通道数据顺序分别是:MIC3 MIC1 MIC4 MIC2。其中,MIC1 是左声道,MIC2 是右声道,MIC3 是参考信号,MIC4 接地没有用。通过上面函数中的 for 循环,就可以调整为模型需要的顺序。
例如,当 i=0 时,结果如下:
(mic_l)buffer[0]=buffer[1];(MIC1)
(mic_r)buffer[1]=buffer[3];(MIC2)
(ref) buffer[2]=buffer[0];(MIC3)
左边是调整后的 buffer,右边是调整前的 buffer。
detect_Task 任务
void detect_Task(void *arg)
{
esp_afe_sr_data_t *afe_data = arg; // 接收参数
int afe_chunksize = afe_handle->get_fetch_chunksize(afe_data); // 获取fetch帧长度
char *mn_name = esp_srmodel_filter(models, ESP_MN_PREFIX, ESP_MN_CHINESE); // 初始化命令词模型
printf("multinet:%s\n", mn_name); // 打印命令词模型名称
esp_mn_iface_t *multinet = esp_mn_handle_from_name(mn_name);
model_iface_data_t *model_data = multinet->create(mn_name, 6000); // 设置唤醒后等待事件 6000代表6000毫秒
esp_mn_commands_clear(); // 清除当前的命令词列表
esp_mn_commands_add(1, "bo fang yin yue"); // 播放音乐
esp_mn_commands_add(2, "zan ting"); // 暂停
esp_mn_commands_add(3, "ji xu"); // 继续
esp_mn_commands_add(4, "shang yi shou"); // 上一首
esp_mn_commands_add(5, "xia yi shou"); // 下一首
esp_mn_commands_add(6, "sheng yin da yi dian"); // 声音大一点
esp_mn_commands_add(7, "sheng yin xiao yi dian"); // 声音小一点
esp_mn_commands_update(); // 更新命令词
int mu_chunksize = multinet->get_samp_chunksize(model_data); // 获取samp帧长度
assert(mu_chunksize == afe_chunksize);
// 打印所有的命令
multinet->print_active_speech_commands(model_data);
printf("------------detect start------------\n");
while (task_flag) {
afe_fetch_result_t* res = afe_handle->fetch(afe_data); // 获取模型输出结果
if (!res || res->ret_value == ESP_FAIL) {
printf("fetch error!\n");
break;
}
if (res->wakeup_state == WAKENET_DETECTED) {
printf("WAKEWORD DETECTED\n");
multinet->clean(model_data); // clean all status of multinet
} else if (res->wakeup_state == WAKENET_CHANNEL_VERIFIED) { // 检测到唤醒词
// play_voice = -1;
afe_handle->disable_wakenet(afe_data); // 关闭唤醒词识别
detect_flag = 1; // 标记已检测到唤醒词
ai_gui_in(); // AI人出现
printf("AFE_FETCH_CHANNEL_VERIFIED, channel index: %d\n", res->trigger_channel_id);
}
if (detect_flag == 1) {
esp_mn_state_t mn_state = multinet->detect(model_data, res->data); // 检测命令词
if (mn_state == ESP_MN_STATE_DETECTING) {
continue;
}
if (mn_state == ESP_MN_STATE_DETECTED) { // 已检测到命令词
esp_mn_results_t *mn_result = multinet->get_results(model_data); // 获取检测词结果
for (int i = 0; i < mn_result->num; i++) { // 打印获取到的命令词
printf("TOP %d, command_id: %d, phrase_id: %d, string:%s prob: %f\n",
i+1, mn_result->command_id[i], mn_result->phrase_id[i], mn_result->string, mn_result->prob[i]);
}
// 根据命令词 执行相应动作
switch (mn_result->command_id[0])
{
case 1: // bo fang yin yue 播放音乐
ai_play();
// lv_event_send(btn_play_pause, LV_EVENT_VALUE_CHANGED, NULL);
break;
case 2: // zan ting 暂停
ai_pause();
break;
case 3: // ji xu 继续
ai_resume();
break;
case 4: // shang yi shou 上一首
ai_prev_music();
break;
case 5: // xia yi shou 下一首
ai_next_music();
break;
case 6: // sheng yin da yi dian 声音大一点
ai_volume_up();
break;
case 7: // sheng yin xiao yi dian 声音小一点
ai_volume_down();
break;
default:
break;
}
printf("\n-----------listening-----------\n");
}
if (mn_state == ESP_MN_STATE_TIMEOUT) { // 达到最大检测命令词时间
esp_mn_results_t *mn_result = multinet->get_results(model_data);
printf("timeout, string:%s\n", mn_result->string);
afe_handle->enable_wakenet(afe_data); // 重新打开唤醒词识别
detect_flag = 0; // 清除标记
printf("\n-----------awaits to be waken up-----------\n");
ai_gui_out(); // AI人退出
continue;
}
}
}
if (model_data) {
multinet->destroy(model_data);
model_data = NULL;
}
printf("detect exit\n");
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
该任务函数,主要完成三件事,一是检测唤醒词,二是检测命令词,三是执行命令词相应的动作。
第 8 行,可以设置被唤醒后,多长时间退出语音控制。
第 10~16 行,可以添加命令词,使用中文拼音即可。
第 34 检测到唤醒词后,第 36 行关闭唤醒词识别,第 38 行,在屏幕上显示一张 gif 图片,提示用户已经检测到唤醒词,可以说命令了。
第 49 行检测到命令词后,第 52 行输出检测到的命令词,第 56~83 行,根据识别到的命令,执行相应的动作。
第 87 行,超时检测后,第 90 行,重新打开唤醒词识别功能,第 93 行删除 gif 图片,提醒用户现在不能说语音命令了。
15.3 例程制作
本例程是在上一章的【11-mp3_player】例程上修改,复制粘贴后,修改例程名称为【12-speech_recognition】。参考例程为 ESP-Skainet 中的 cn_speech_commands_recognition 例程。把我们的例程和 ESP-Skainet 工程,都使用 VSCode 打开。
本例程需要使用 esp-sr 组件,所以需要在 idf_component.yml 文件中,添加如下代码:
espressif/esp-sr: '~1.6.0' # 语音识别
在一级目录 CMakeLists.txt 文件中,修改工程名为 speech_recognition。
project(speech_recognition)
在 main 文件夹下,添加两个文件,分别是 app_sr.c 和 app_sr.h,用来存放语音识别相关代码。在 main 文件夹下的 CMakeLists.txt 文件中,把 app_sr.c 添加到 idf_component_register 的 SRCS。
idf_component_register(SRCS "app_sr.c" "esp32_s3_szp.c" "main.c" "app_ui.c"
INCLUDE_DIRS ".")
2
把参考例程 cn_speech_commands_recognition 中的 main.c 文件中的内容,全部复制粘贴到我们例程的 app_sr.c 文件中。
把 app_sr.c 文件中的 app_main 函数名称修改为 app_sr_init。
把初始化音频硬件的语句去掉,因为我们在主函数中已经初始化了,同时把对应头文件也注释或删掉。
//#include "esp_board_init.h"
把函数里面的注释部分全部删除,再把条件编译以及里面的语句删除。
再添加一个打印输出唤醒名称的语句,并添加用于打印的 TAG 定义。
最后修改好的函数如下所示:
static const char *TAG = "app_sr";
void app_sr_init(void)
{
models = esp_srmodel_init("model");
afe_handle = (esp_afe_sr_iface_t *)&ESP_AFE_SR_HANDLE;
afe_config_t afe_config = AFE_CONFIG_DEFAULT();
afe_config.wakenet_model_name = esp_srmodel_filter(models, ESP_WN_PREFIX, NULL);;
afe_data = afe_handle->create_from_config(&afe_config);
ESP_LOGI(TAG, "wakenet:%s", afe_config.wakenet_model_name); // 打印唤醒名称
task_flag = 1;
xTaskCreatePinnedToCore(&detect_Task, "detect", 8 * 1024, (void*)afe_data, 5, NULL, 1);
xTaskCreatePinnedToCore(&feed_Task, "feed", 8 * 1024, (void*)afe_data, 5, NULL, 0);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
该文件中,还有 3 个函数,分别是 play_music、feed_task、detect_task,把 play_music 函数删除,我们这里用不到。同时把 play_music 用到的头文件也去掉后注释。
//#include "speech_commands_action.h"
接下来修改 feed_task 函数,这个函数,用来获取 MIC 的 I2S 数据,并把 I2S 数据输入到模型。
这里使用的是 malloc 分配内存,我们修改成使用 heap_caps_malloc 函数,把内存分配到外部 PSRAM 中。
最后结果如下:
void feed_Task(void *arg)
{
esp_afe_sr_data_t *afe_data = arg;
int audio_chunksize = afe_handle->get_feed_chunksize(afe_data);
int nch = afe_handle->get_channel_num(afe_data);
int feed_channel = esp_get_feed_channel();
assert(nch <= feed_channel);
int16_t *i2s_buff = heap_caps_malloc(audio_chunksize * sizeof(int16_t) * feed_channel, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
assert(i2s_buff);
while (task_flag) {
esp_get_feed_data(false, i2s_buff, audio_chunksize * sizeof(int16_t) * feed_channel);
afe_handle->feed(afe_data, i2s_buff);
}
if (i2s_buff) {
free(i2s_buff);
i2s_buff = NULL;
}
vTaskDelete(NULL);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里面有两个函数,esp_get_feed_channel()
函数和 esp_get_feed_data()
函数,需要在 esp32_s3_szp.c 文件中,定义这两个函数,如下所示:
int esp_get_feed_channel(void)
{
return ADC_I2S_CHANNEL;
}
esp_err_t esp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len)
{
esp_err_t ret = ESP_OK;
int audio_chunksize = buffer_len / (sizeof(int16_t) * ADC_I2S_CHANNEL);
ret = esp_codec_dev_read(record_dev_handle, (void *)buffer, buffer_len);
if (!is_get_raw_channel) {
for (int i = 0; i < audio_chunksize; i++) {
int16_t ref = buffer[4 * i + 0];
buffer[3 * i + 0] = buffer[4 * i + 1];
buffer[3 * i + 1] = buffer[4 * i + 3];
buffer[3 * i + 2] = ref;
}
}
return ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后在 esp32_s3_szp.h 文件中,声明这两个函数,并把 ADC_I2S_CHANNEL 定义为 4,因为有 4 个麦克风通道。如下所示:
#define ADC_I2S_CHANNEL 4
int esp_get_feed_channel(void);
esp_err_t esp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len);
2
3
4
在 app_sr.c 文件中,添加 esp32_s3_szp.h 头文件。
#include "esp32_s3_szp.h"
接下来修改 detect_task 函数,这个函数用来检测唤醒词和命令词。
从上往下修改。
找到下面这条添加命令的语句,这里是添加默认命令,我们把它删除,因为我们要自定义命令。
esp_mn_commands_update_from_sdkconfig(multinet, model_data); // Add speech commands from sdkconfig
然后在这条语句位置,添加下面的语句:
esp_mn_commands_clear(); // 清除当前的命令词列表
esp_mn_commands_add(1, "bo fang yin yue"); // 播放音乐
esp_mn_commands_add(2, "zan ting"); // 暂停
esp_mn_commands_add(3, "ji xu"); // 继续
esp_mn_commands_add(4, "shang yi shou"); // 上一首
esp_mn_commands_add(5, "xia yi shou"); // 下一首
esp_mn_commands_add(6, "sheng yin da yi dian"); // 声音大一点
esp_mn_commands_add(7, "sheng yin xiao yi dian"); // 声音小一点
esp_mn_commands_update(); // 更新命令词
2
3
4
5
6
7
8
9
使用这些语句,需要添加头文件 esp_mn_speech_commands.h。
#include "esp_mn_speech_commands.h"
再往下,把注释掉的两条语句删除,如下:
// FILE *fp = fopen("/sdcard/out1", "w");
// if (fp == NULL) printf("can not open file\n");
2
再往下,条件编译部分,保留 ESP32S3 的语句,其它的都去掉。
原来是这样:
#if CONFIG_IDF_TARGET_ESP32
if (res->wakeup_state == WAKENET_DETECTED) {
printf("wakeword detected\n");
play_voice = -1;
detect_flag = 1;
afe_handle->disable_wakenet(afe_data);
printf("-----------listening-----------\n");
}
#elif CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32P4
if (res->wakeup_state == WAKENET_DETECTED) {
printf("WAKEWORD DETECTED\n");
multinet->clean(model_data); // clean all status of multinet
} else if (res->wakeup_state == WAKENET_CHANNEL_VERIFIED) {
play_voice = -1;
detect_flag = 1;
printf("AFE_FETCH_CHANNEL_VERIFIED, channel index: %d\n", res->trigger_channel_id);
}
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
现在改成:
if (res->wakeup_state == WAKENET_DETECTED) {
printf("WAKEWORD DETECTED\n");
multinet->clean(model_data); // clean all status of multinet
} else if (res->wakeup_state == WAKENET_CHANNEL_VERIFIED) {
play_voice = -1;
detect_flag = 1;
printf("AFE_FETCH_CHANNEL_VERIFIED, channel index: %d\n", res->trigger_channel_id);
}
2
3
4
5
6
7
8
再往下,在已验证唤醒词的 if 条件语句里面,添加“关闭唤醒”和“出现 AI 人”的语句,同时把 paly_voice = -1 注释掉,用不着这个变量,这个变量在之前删除的 play_music 函数里面有用,因为 play_music 函数已经删除了,这个变量也就没有用了,同时把这个变量的定义,也一并注释掉,或删除。
} else if (res->wakeup_state == WAKENET_CHANNEL_VERIFIED) { // 检测到唤醒词
// play_voice = -1;
afe_handle->disable_wakenet(afe_data); // 关闭唤醒词识别
detect_flag = 1; // 标记已检测到唤醒词
ai_gui_in(); // AI人出现
printf("AFE_FETCH_CHANNEL_VERIFIED, channel index: %d\n", res->trigger_channel_id);
}
2
3
4
5
6
7
关闭唤醒词,是因为已经检测到唤醒了,如果不关闭,还会检测唤醒词,关了减轻 CPU 负担。
ai_gui_in()
函数用来显示一张 gif 图标,提示用户检测到了唤醒。这个函数后续会定义在文件里面。
再往下,在检测到命令词的 for 循环语句后面,添加执行相应动作的程序,如下所示:
if (mn_state == ESP_MN_STATE_DETECTED) {
esp_mn_results_t *mn_result = multinet->get_results(model_data);
for (int i = 0; i < mn_result->num; i++) {
printf("TOP %d, command_id: %d, phrase_id: %d, string:%s prob: %f\n",
i+1, mn_result->command_id[i], mn_result->phrase_id[i], mn_result->string, mn_result->prob[i]);
}
// 根据命令词 执行相应动作
switch (mn_result->command_id[0])
{
case 1: // bo fang yin yue 播放音乐
ai_play();
// lv_event_send(btn_play_pause, LV_EVENT_VALUE_CHANGED, NULL);
break;
case 2: // zan ting 暂停
ai_pause();
break;
case 3: // ji xu 继续
ai_resume();
break;
case 4: // shang yi shou 上一首
ai_prev_music();
break;
case 5: // xia yi shou 下一首
ai_next_music();
break;
case 6: // sheng yin da yi dian 声音大一点
ai_volume_up();
break;
case 7: // sheng yin xiao yi dian 声音小一点
ai_volume_down();
break;
default:
break;
}
printf("\n-----------listening-----------\n");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
这里面添加的 ai_play()
等函数,后续需要在 app_ui.c 文件中实现。
再往下,在超时发生后的语句里面,添加“AI 人退出”的语句,表示不可以再说命令词了。
if (mn_state == ESP_MN_STATE_TIMEOUT) {
esp_mn_results_t *mn_result = multinet->get_results(model_data);
printf("timeout, string:%s\n", mn_result->string);
afe_handle->enable_wakenet(afe_data);
detect_flag = 0;
ai_gui_out(); // AI人退出
printf("\n-----------awaits to be waken up-----------\n");
continue;
}
2
3
4
5
6
7
8
9
这里的 ai_gui_out()函数,和前面的 ai_gui_in()函数,定义到 app_ui.c 文件中,放到文件最后面就行,如下所示:
// AI人动画
LV_IMG_DECLARE(img_bilibili120);
lv_obj_t *gif_start;
// AI人出现在屏幕
void ai_gui_in(void)
{
lvgl_port_lock(0);
gif_start = lv_gif_create(lv_scr_act());
lv_gif_set_src(gif_start, &img_bilibili120);
lv_obj_align(gif_start, LV_ALIGN_CENTER, 0, 0);
lvgl_port_unlock();
}
// AI人退出屏幕
void ai_gui_out(void)
{
lvgl_port_lock(0);
lv_obj_del(gif_start);
lvgl_port_unlock();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
img_bilibili120 图片文件,是我们在实战派 C3 开发板例程【桌面对话助手】中制作好的文件,我们可以直接把这个工程中的 img_bilibili120.c 文件复制到 main 文件夹中。关于图片文件的制作,请看实战派 C3 开发板这个例程对应的文档教程。
添加好文件后,在 main 下的 CMakeLists.txt 文件中,把 img_bilibili120.c 添加到 SRCS 中。
idf_component_register(SRCS "app_sr.c" "esp32_s3_szp.c" "main.c" "app_ui.c" "img_bilibili120.c"
INCLUDE_DIRS ".")
2
根据命令执行相应的播放、暂停等函数,也定义到 app_ui.c 文件中,如下所示:
// AI播放音乐
void ai_play(void)
{
int index = file_iterator_get_index(file_iterator);
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
lvgl_port_lock(0);
lv_label_set_text_static(label_play_pause, LV_SYMBOL_PAUSE); // 显示图标
lv_obj_add_state(btn_play_pause, LV_STATE_CHECKED); // 按键设置为选中状态
lvgl_port_unlock();
}
// AI暂停
void ai_pause(void)
{
audio_player_pause();
lvgl_port_lock(0);
lv_label_set_text_static(label_play_pause, LV_SYMBOL_PLAY); // 显示图标
lv_obj_clear_state(btn_play_pause, LV_STATE_CHECKED); // 清除按键的选中状态
lvgl_port_unlock();
}
// AI继续
void ai_resume(void)
{
audio_player_resume();
lvgl_port_lock(0);
lv_label_set_text_static(label_play_pause, LV_SYMBOL_PAUSE); // 显示图标
lv_obj_add_state(btn_play_pause, LV_STATE_CHECKED); // 按键设置为选中状态
lvgl_port_unlock();
}
// AI上一首
void ai_prev_music(void)
{
// 指向上一首音乐
file_iterator_prev(file_iterator);
// 修改当前的音乐名称
int index = file_iterator_get_index(file_iterator);
lvgl_port_lock(0);
lv_dropdown_set_selected(music_list, index);
lv_obj_t *label_title = (lv_obj_t *) music_list->user_data;
lv_label_set_text_static(label_title, file_iterator_get_name_from_index(file_iterator, index));
lvgl_port_unlock();
// 执行音乐事件
audio_player_state_t state = audio_player_get_state();
printf("prev_next_state=%d\n", state);
if (state == AUDIO_PLAYER_STATE_IDLE) {
// Nothing to do
}else if (state == AUDIO_PLAYER_STATE_PAUSE){ // 如果当前正在暂停歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
audio_player_pause();
} else if (state == AUDIO_PLAYER_STATE_PLAYING) { // 如果当前正在播放歌曲
// 播放歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
}
}
// AI下一首
void ai_next_music(void)
{
// 指向上一首音乐
file_iterator_next(file_iterator);
// 修改当前的音乐名称
int index = file_iterator_get_index(file_iterator);
lvgl_port_lock(0);
lv_dropdown_set_selected(music_list, index);
lv_obj_t *label_title = (lv_obj_t *) music_list->user_data;
lv_label_set_text_static(label_title, file_iterator_get_name_from_index(file_iterator, index));
lvgl_port_unlock();
// 执行音乐事件
audio_player_state_t state = audio_player_get_state();
printf("prev_next_state=%d\n", state);
if (state == AUDIO_PLAYER_STATE_IDLE) {
// Nothing to do
}else if (state == AUDIO_PLAYER_STATE_PAUSE){ // 如果当前正在暂停歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
audio_player_pause();
} else if (state == AUDIO_PLAYER_STATE_PLAYING) { // 如果当前正在播放歌曲
// 播放歌曲
ESP_LOGI(TAG, "playing index '%d'", index);
play_index(index);
}
}
// AI声音大一点
void ai_volume_up(void)
{
if (g_sys_volume < 100){
g_sys_volume = g_sys_volume + 5;
if (g_sys_volume > 100){
g_sys_volume = 100;
}
bsp_codec_volume_set(g_sys_volume, NULL); // 设置声音大小
lvgl_port_lock(0);
lv_slider_set_value(volume_slider, g_sys_volume, LV_ANIM_ON); // 设置slider
lvgl_port_unlock();
}
ESP_LOGI(TAG, "volume '%d'", g_sys_volume);
}
// AI声音小一点
void ai_volume_down(void)
{
if (g_sys_volume > 0){
if (g_sys_volume < 5){
g_sys_volume = 0;
}else{
g_sys_volume = g_sys_volume - 5;
}
bsp_codec_volume_set(g_sys_volume, NULL); // 设置声音大小
lvgl_port_lock(0);
lv_slider_set_value(volume_slider, g_sys_volume, LV_ANIM_ON); // 设置slider
lvgl_port_unlock();
}
ESP_LOGI(TAG, "volume '%d'", g_sys_volume);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
这几个函数需要修改按钮、名称、滑动条,所以需要把这 3 个声明为全局变量。
lv_obj_t *label_play_pause;
lv_obj_t *btn_play_pause;
lv_obj_t *volume_slider;
2
3
然后进入 music_ui 函数中,把这几个 obj 的定义去掉,改成直接用。
label_play_pause 的语句原来如下:
lv_obj_t *label_play_pause = lv_label_create(btn_play_pause);
修改成:
label_play_pause = lv_label_create(btn_play_pause);
其它两个类似。
然后把这几个函数声明在 app_ui.h 中,如下所示:
void ai_gui_in(void);
void ai_gui_out(void);
void ai_play(void);
void ai_pause(void);
void ai_resume(void);
void ai_prev_music(void);
void ai_next_music(void);
void ai_volume_up(void);
void ai_volume_down(void);
2
3
4
5
6
7
8
9
10
在 app_sr.c 文件中,包含 app_ui.h 头文件。
#include "app_ui.h"
在 esp32_s3_szp.c 文件中,把 bsp_codec_set_fs()函数和 bsp_codec_init()函数中,关于麦克风部分的初始化和采样率设置等之前被注释掉的语句都放开。
在 app_sr.h 文件中,加入下面语句,声明 app_sr_init 函数。
#pragma once
// #include <stdbool.h>
void app_sr_init(void);
2
3
4
5
在 main.c 文件里面,调用 app_sr_init 初始化函数,并添加 app_sr.h 头文件。
#include <stdio.h>
#include "esp32_s3_szp.h"
#include "app_ui.h"
#include "app_sr.h"
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lvgl_start(); // 初始化液晶屏lvgl接口
bsp_spiffs_mount(); // SPIFFS文件系统初始化
bsp_codec_init(); // 音频初始化
mp3_player_init(); // MP3播放器初始化
app_sr_init(); // 初始化语音识别
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在分区表中,添加模型存放的区域,如下面第 7 行是添加的。
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 24k
phy_init, data, phy, 0xf000, 4k
factory, app, factory, , 3M
storage, data, spiffs, , 3M
model, data, spiffs, , 5168K,
2
3
4
5
6
7
把你制作好的 mp3 音乐放到 spiffs 文件里面,注意,要修改成 32000 采样率的音乐。或者先复制例程中自带的音乐到你的文件夹,先验证你的程序。
在 sdkconfig.default 文件中,添加 lvgl 的 gif 库。
CONFIG_LV_USE_GIF=y
在 esp32_s3_szp.h 文件中,把音频采样率改成 16000,位宽改成 32,通道改成 2。
#define CODEC_DEFAULT_SAMPLE_RATE (16000)
#define CODEC_DEFAULT_BIT_WIDTH (32)
#define CODEC_DEFAULT_ADC_VOLUME (24.0)
#define CODEC_DEFAULT_CHANNEL (2)
2
3
4
在 app_ui.c 文件中,把_audio_player_std_clock 函数中的修改采样率的语句注释掉,不让要它在播放音乐的时候修改采样率等信息。
static esp_err_t _audio_player_std_clock(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
{
esp_err_t ret = ESP_OK;
// ret = bsp_codec_set_fs(rate, bits_cfg, ch);
return ret;
}
2
3
4
5
6
7
8
在 app_ui.c 文件中,把 mp3_player_init 函数中的优先级改成 6,然后再把这个播放任务给了 CPU1 处理,否则,在 AI 人出现的时候,如果正在播放音乐,音乐会有点卡。
// mp3播放器初始化
void mp3_player_init(void)
{
// 获取文件信息
file_iterator = file_iterator_new(SPIFFS_BASE);
assert(file_iterator != NULL);
// 初始化音频播放
player_config.mute_fn = _audio_player_mute_fn;
player_config.write_fn = _audio_player_write_fn;
player_config.clk_set_fn = _audio_player_std_clock;
player_config.priority = 6;
player_config.coreID = 1;
ESP_ERROR_CHECK(audio_player_new(player_config));
ESP_ERROR_CHECK(audio_player_callback_register(_audio_player_callback, NULL));
// 显示界面
music_ui();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
选择目标芯片为 esp32s3,选择串口号、串口下载方式,然后“一键三连”按钮编译下载看结果。