正点原子ESP32S3+ES8388+ESP-SR实现离线语音唤醒

正点原子ESP32S3+ES8388+ESP-SR实现离线语音唤醒

首先吐槽一下ESP-SR的官方文档写得跟💩一样,下个源码还得去github,另外网上能找到匹配的资料也很少,正点原子的例程里也没有语音唤醒的部分。最后还是参照了B站的一个博主,不仅贴出了自己的源码,还对代码进行了逐行的讲解,简洁明了,非常贴心。

视频地址:【ESP32 语音识别教程【ESP-SR】】https://www.bilibili.com/video/BV1vf421z7r8?vd_source=8cd57dc0cbfa678010de3add3e61ac00

官方github的参考代码地址:esp-skainet/examples/cn_speech_commands_recognition at master · espressif/esp-skainet · GitHub

正点原子DNESP32S3开发板的资料地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

但就算这样还是花了我三天时间,其中两天花在了一个巨坑上(B站博主讲到了但是我没贯彻到底),直到最后心如死灰了,程序又莫名奇妙识别成功了,不得不说是“柳暗花明又一村”了。

准备工作

硬件:正点原子DNESP32S3,使用ES8366作为音频输入输出芯片

IDF版本:5.1.2

SR版本:2.1.5(B站视频博主使用的是1.7版本,和2.0+版本在初始化上略有不同)

SR通过idf.py add-dependency "espressif/esp-sr^2.1.5"命令安装。

相关代码

对代码的几个重要的说明:

  1. 环境的配置部分可以参考视频博主,开发板相关的I2S初始化和ES8388部分直接来自正点原子的recoder例程,ESP-SR的代码来自视频博主。
  2. 代码我逐条加了注释,以供参考。(注释中标注了“新增”的部分是我在博主的基础上修改的)
  3. 板子以及LCD相关的代码我也做了注释,可能影响观感
  4. 博主使用的ESP-SR的1.0+版本中可能必须要求有三通道数据,左+右+回采,但是新的版本中可以自行选择声道数量,所以我去掉了回采(不用手动造一个声道出来)。
  5. 原本的代码中没有给IDLE线程喂狗的空隙,导致程序不断重启,所以我在采集和检测循环里都加入了喂狗间隙vTaskDelay(pdMS_TO_TICKS(1));

关于整个过程里最大的坑:

ESP-SR要求的采集到的参数必须是采样率16000Hz,16位采样精度

所以重点来了,要说三遍:

I2S的输入和输出的采样频率都必须是16000Hz

I2S的输入和输出的采样频率都必须是16000Hz

I2S的输入和输出的采样频率都必须是16000Hz

// app-sr.c
/*
 * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include "esp_check.h"
#include "esp_err.h"
#include "esp_log.h"
#include "app_sr.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_models.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"

// #include "app_play_music.h"
#include "app_sr_handler.h"
#include "model_path.h"
#include "esp_mn_speech_commands.h"
#include "esp_process_sdkconfig.h"
// #include "bsp_board_extra.h"
#include "i2s.h"

#define I2S_CHANNEL_NUM     (2)

static const char *TAG = "app_sr";

static model_iface_data_t       *model_data     = NULL;
static const esp_mn_iface_t     *multinet       = NULL;
static const esp_afe_sr_iface_t *afe_handle     = NULL;
static QueueHandle_t            g_result_que    = NULL;
static srmodel_list_t           *models         = NULL;

const char *cmd_phoneme[12] = {
    "da kai kong qi jing hua qi",
    "guan bi kong qi jing hua qi",
    "da kai tai deng",
    "guan bi tai deng",
    "tai deng tiao liang",
    "tai deng tiao an",
    "da kai deng dai",
    "guan bi deng dai",
    "bo fang yin yue",
    "ting zhi bo fang",
    "da kai shi jian",
    "da kai ri li"
};

// 音频采集任务
static void audio_feed_task(void *pvParam)
{
    size_t bytes_read = 0;
    esp_afe_sr_data_t *afe_data = (esp_afe_sr_data_t *) pvParam;
    int audio_chunksize = afe_handle->get_feed_chunksize(afe_data);// 获取单帧数据大小
    ESP_LOGI(TAG, "audio_chunksize=%d, feed_channel=%d", audio_chunksize, 2);

    /* Allocate audio buffer and check for result */
    // 参考单帧大小malloc一个音频buffer,如果有psram则malloc在psram上
    int16_t *audio_buffer = heap_caps_malloc(audio_chunksize * sizeof(int16_t) * 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (NULL == audio_buffer) {
        esp_system_abort("No mem for audio buffer");
    }

    while (true) {
        /* Read audio data from I2S bus */
        // 持续从IIS读取音频数据
        bytes_read = i2s_rx_read((uint8_t *)audio_buffer, audio_chunksize * 2 * sizeof(int16_t));
        //printf("IIS读取了%d\n", bytes_read);
        if (bytes_read == 0 ) {
            ESP_LOGE(TAG, "======== bsp_extra_i2s_read failed ==========");
        }

        /* Channel Adjust */
        // 声学前端需要三声道,左声道+右声道+回采,IIS只收集左右声道,所以加入全0的第三声道
        // for (int  i = audio_chunksize - 1; i >= 0; i--) {
        //     audio_buffer[i * 3 + 2] = 0;
        //     audio_buffer[i * 3 + 1] = audio_buffer[i * 2 + 1];
        //     audio_buffer[i * 3 + 0] = audio_buffer[i * 2 + 0];
        // }

        /* Feed samples of an audio stream to the AFE_SR */
        // 如果读取成功则传给声学前端进行检测
        afe_handle->feed(afe_data, audio_buffer);
        vTaskDelay(pdMS_TO_TICKS(1)); 
    }

    /* Clean up if audio feed ends */
    afe_handle->destroy(afe_data);

    /* Task never returns */
    vTaskDelete(NULL);
}

// 音频检测任务
static void audio_detect_task(void *pvParam)
{
    bool detect_flag = false;
    esp_afe_sr_data_t *afe_data = (esp_afe_sr_data_t *) pvParam;

    /* Check if audio data has same chunksize with multinet */
    // 确认音频的采样大小和命令词模型的采样大小是否一致,默认每32ms喂一次
    int afe_chunksize = afe_handle->get_fetch_chunksize(afe_data);
    int mu_chunksize = multinet->get_samp_chunksize(model_data);
    // assert(mu_chunksize == afe_chunksize);
    ESP_LOGI(TAG, "------------detect start------------\n");

    while (true) {
        // 从声学前端获取采集并处理后的音频数据
        afe_fetch_result_t *res = afe_handle->fetch(afe_data);
        // 新增日志
        // ESP_LOGI(TAG, "fetch result: %p, ret_value: %d", res, res ? res->ret_value : -1);
        if (!res || res->ret_value == ESP_FAIL) {
            ESP_LOGE(TAG, "fetch error!");
            continue;
        }

        // if (res->vad_state == 1) {
        //     ESP_LOGI(TAG, "VAD detected voice - 可能正在说话\n");
        // }

        // 打印唤醒状态(例如:0=未检测,1=检测到)
        //ESP_LOGI(TAG, "wakeup_state: %d, VAD state: %d", res->wakeup_state, res->vad_state);

        // 如果检测到唤醒词,就把检测结果通过消息队列发送给处理任务
        if (res->wakeup_state == WAKENET_DETECTED) {
            ESP_LOGI(TAG, LOG_BOLD(LOG_COLOR_GREEN) "Wakeword detected");
            sr_result_t result = {
                .wakenet_mode = WAKENET_DETECTED,
                .state = ESP_MN_STATE_DETECTING,
                .command_id = 0,
            };
            xQueueSend(g_result_que, &result, 10);
        } else if (res->wakeup_state == WAKENET_CHANNEL_VERIFIED) {
            // 唤醒后,延时3帧开启命令词识别,关闭唤醒词识别
            // ESP_LOGI(TAG, LOG_BOLD(LOG_COLOR_GREEN) "Channel verified");
            // detect_flag = true;
            // afe_handle->disable_wakenet(afe_data);
        }
		
        if(detect_flag)
        {
            
        }
        vTaskDelay(pdMS_TO_TICKS(1)); 
    }

    /* Clean up if audio feed ends */
    afe_handle->destroy(afe_data);

    /* Task never returns */
    vTaskDelete(NULL);
}

esp_err_t app_sr_start(void)
{
    // 创建识别结果队列
    g_result_que = xQueueCreate(1, sizeof(sr_result_t));
    ESP_RETURN_ON_FALSE(NULL != g_result_que, ESP_ERR_NO_MEM, TAG, "Failed create result queue");

    // 获取所有模型列表
    models = esp_srmodel_init("model");

    // 新增:检查models是否初始化成功
    ESP_RETURN_ON_FALSE(models != NULL && models->num > 0, ESP_FAIL, TAG, "Failed to load models from 'model' path");

    // 新增:打印所有加载的模型名称,确认是否有唤醒词模型(通常以"wn"开头)
    for (int i = 0; i < models->num; i++) {
        ESP_LOGI(TAG, "Loaded model: %s", models->model_name[i]);
    }

    // 用默认配置来初始化一个声学前端句柄以及AFE的配置结构体
    // 已弃用
    // afe_handle = &ESP_AFE_SR_HANDLE;
    // afe_config_t afe_config = AFE_CONFIG_DEFAULT();

    afe_config_t *afe_config = afe_config_init("MM", models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
    afe_config_print(afe_config); // print all configurations
    afe_handle = esp_afe_handle_from_config(afe_config);

    // 从模型队列中过滤出之前配置好的唤醒词模型名字
    afe_config->wakenet_model_name = esp_srmodel_filter(models, ESP_WN_PREFIX, NULL);
    afe_config->aec_init = true;    // 为了节省资源,关掉回声消除

    // 新增:VAD(语音活动检测):优化参数过滤短时噪声
    afe_config->vad_init = true;
    afe_config->vad_mode = VAD_MODE_2; // 中等灵敏度
    afe_config->vad_model_name = NULL; // 使用默认WebRTC VAD
    afe_config->vad_min_speech_ms = 200; // 最小语音持续200ms(过滤敲击声等)
    afe_config->vad_min_noise_ms = 800; // 最小噪声持续800ms
    afe_config->vad_delay_ms = 128;
    afe_config->vad_mute_playback = false;
    afe_config->vad_enable_channel_trigger = false;

    // 新增:AGC(自动增益控制):平衡音量,避免噪声放大
    afe_config->agc_init = true;
    afe_config->agc_mode = AFE_AGC_MODE_WAKENET; // 适配唤醒场景
    afe_config->agc_compression_gain_db = 10; // 适度增益(10dB)
    afe_config->agc_target_level_dbfs = 8; // 目标电平-8dBFS
    
    afe_config->afe_ringbuf_size = 100;  // 从50帧增至100帧(根据内存情况可调整至200)

    // 使用这个AFE的配置结构体创建AFE数据对象afe_data,可以用来保存AFE相关的所有状态信息
    esp_afe_sr_data_t *afe_data = afe_handle->create_from_config(afe_config);
    ESP_LOGI(TAG, "load wakenet:%s", afe_config->wakenet_model_name); // 打印唤醒词的名字确认一下

    /*********************************** 唤醒词模型加载完成 ***********************************/
    
    for (int i = 0; i < models->num; i++) {
        ESP_LOGI(TAG, "Current Model:%s", models->model_name[i]);
    }

    char *mn_name = esp_srmodel_filter(models, ESP_MN_CHINESE, NULL);
    if (NULL == mn_name) {
        ESP_LOGE(TAG, "No multinet model found");
        return ESP_FAIL;
    }

    multinet = esp_mn_handle_from_name(mn_name);
    model_data = multinet->create(mn_name, 5760);
    ESP_LOGI(TAG, "load multinet:%s", mn_name);

    /*********************************** 命令词模型加载完成 ***********************************/

    esp_mn_commands_clear();

    for (int i = 0; i < sizeof(cmd_phoneme) / sizeof(cmd_phoneme[0]); i++) {
        esp_mn_commands_add(i, (char *)cmd_phoneme[i]);
    }

    esp_mn_commands_update();
    esp_mn_commands_print();
    multinet->print_active_speech_commands(model_data);

    /*********************************** 注册语音指令完成 ***********************************/

    // 采集音频数据,core1,高优先级
    BaseType_t ret_val = xTaskCreatePinnedToCore(audio_feed_task, "Feed Task", 4 * 1024, afe_data, 5, NULL, 1);
    ESP_RETURN_ON_FALSE(pdPASS == ret_val, ESP_FAIL, TAG,  "Failed create audio feed task");

    // 检测音频数据,core0,高优先级
    ret_val = xTaskCreatePinnedToCore(audio_detect_task, "Detect Task", 6 * 1024, afe_data, 5, NULL, 0);
    ESP_RETURN_ON_FALSE(pdPASS == ret_val, ESP_FAIL, TAG,  "Failed create audio detect task");

    // 处理检测结果,不需要高优先级 
    ret_val = xTaskCreatePinnedToCore(sr_handler_task, "SR Handler Task", 4 * 1024, g_result_que, 1, NULL, 1);
    ESP_RETURN_ON_FALSE(pdPASS == ret_val, ESP_FAIL, TAG,  "Failed create audio handler task");

    return ESP_OK;
}

esp_err_t app_sr_reset_command_list(char *command_list)
{
    char *err_id = heap_caps_malloc(1024, MALLOC_CAP_SPIRAM);
    ESP_RETURN_ON_FALSE(NULL != err_id, ESP_ERR_NO_MEM, TAG,  "memory is not enough");
    free(err_id);
    return ESP_OK;
}
// app-sr.h

/*
 * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */

#pragma once

#include <stdbool.h>
#include "esp_err.h"
#include "esp_afe_sr_models.h"
#include "esp_mn_models.h"

#ifdef __cplusplus
extern "C" {
#endif

#define SR_CONTINUE_DET 1

typedef struct {
    wakenet_state_t     wakenet_mode;
    esp_mn_state_t      state;
    int                 command_id;
} sr_result_t;

/**
 * @brief Start speech recognition task
 *
 * @param record_en Record audio to SD crad if set to `true`
 * @return
 *    - ESP_OK: Success
 *    - ESP_ERR_NO_MEM: No enough memory for speech recognition
 *    - Others: Fail
 */
esp_err_t app_sr_start(void);

/**
 * @brief Reset command list
 *
 * @param command_list New command string
 * @return
 *    - ESP_OK: Success
 *    - ESP_ERR_NO_MEM: No enough memory for err_id string
 *    - Others: Fail
 */
esp_err_t app_sr_reset_command_list(char *command_list);

#ifdef __cplusplus
}
#endif
// app_sr_handler.c
/*
 * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */
#include <string.h>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_check.h"
#include "app_sr.h"
#include "app_sr_handler.h"
#include "esp_afe_sr_iface.h"
// #include "app_mqtt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
// #include "app_play_music.h"
// #include "ui/ui.h"

static const char *TAG = "app_sr_handler";

// 采集结果处理任务
void sr_handler_task(void *pvParam)
{
    QueueHandle_t xQueue = (QueueHandle_t) pvParam;

    while (true) {
        sr_result_t result;
        xQueueReceive(xQueue, &result, portMAX_DELAY); // 持续获取检测结果,如果队列空的话会阻塞

        ESP_LOGI(TAG, "cmd:%d, wakemode:%d,state:%d", result.command_id, result.wakenet_mode, result.state);

        if (ESP_MN_STATE_TIMEOUT == result.state) {
            ESP_LOGI(TAG, "timeout");
            // lv_obj_clear_flag(ui_cat_gif, LV_OBJ_FLAG_HIDDEN);
            // lv_obj_add_flag(ui_Image1, LV_OBJ_FLAG_HIDDEN);
            // lv_obj_add_flag(ui_Labelwakenet, LV_OBJ_FLAG_HIDDEN);
            continue;
        }

        // 检测到唤醒词
        if (WAKENET_DETECTED == result.wakenet_mode) {
            ESP_LOGI(TAG, "wakenet detected");

            /* Modify UI */
            // lv_obj_add_flag(ui_cat_gif, LV_OBJ_FLAG_HIDDEN);
            // lv_obj_clear_flag(ui_Image1, LV_OBJ_FLAG_HIDDEN);
            // lv_label_set_text(ui_Labelwakenet, "请讲!");
            // lv_obj_clear_flag(ui_Labelwakenet, LV_OBJ_FLAG_HIDDEN);
            // _ui_screen_change(&ui_Screen1, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Screen4_screen_init);
            continue;
        }

        // 检测到命令词,根据ID进行处理
        if (ESP_MN_STATE_DETECTED & result.state) {
            ESP_LOGI(TAG, "mn detected");

            // switch (result.command_id) {
            //     case 0:
            //         ESP_LOGI(TAG, "Turn on the air purifier");
            //         lv_label_set_text(ui_Labelwakenet, "已打开");
            //         mqtt_air_purifer_on();
            //         break;
            //     case 1:
            //         ESP_LOGI(TAG, "Turn off the air purifier");
            //         mqtt_air_purifer_off();
            //         lv_label_set_text(ui_Labelwakenet, "已关闭");
            //         break;
            //     case 2:
            //         ESP_LOGI(TAG, "Turn On the Lamp");
            //         mqtt_lamp_on();
            //         lv_label_set_text(ui_Labelwakenet, "已打开");
            //         break;
            //     case 3:
            //         ESP_LOGI(TAG, "Turn Off the Lamp");
            //         mqtt_lampr_off();
            //         lv_label_set_text(ui_Labelwakenet, "已关闭");
            //         break;
            //     case 4:
            //         ESP_LOGI(TAG, "Turn Lamp Brighter");
            //         mqtt_lamp_brighter();
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         break;
            //     case 5:
            //         ESP_LOGI(TAG, "Turn Lamp Dimmer");
            //         mqtt_lampr_dimmer();
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         break;
            //     case 6:
            //         ESP_LOGI(TAG, "Turn on the LED Strip");
            //         mqtt_led_on();
            //         lv_label_set_text(ui_Labelwakenet, "已打开");
            //         break;
            //     case 7:
            //         ESP_LOGI(TAG, "Turn off the LED Strip");
            //         mqtt_led_off();
            //         lv_label_set_text(ui_Labelwakenet, "已关闭");
            //         break;
            //     case 8:
            //         ESP_LOGI(TAG, "Play Music");
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         _ui_screen_change(&ui_Screen4, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Screen4_screen_init);
            //         music_play_resume();
            //         break;
            //     case 9:
            //         ESP_LOGI(TAG, "Stop Music");
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         _ui_screen_change(&ui_Screen1, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Screen4_screen_init);
            //         music_play_pause();
            //         break;
            //     case 10:
            //         ESP_LOGI(TAG, "Show Time");
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         _ui_screen_change(&ui_Screen1, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Screen1_screen_init);
            //         break;
            //     case 11:
            //         ESP_LOGI(TAG, "Show Calander");
            //         lv_label_set_text(ui_Labelwakenet, "好的!");
            //         _ui_screen_change(&ui_Screen3, LV_SCR_LOAD_ANIM_FADE_ON, 100, 0, &ui_Screen1_screen_init);
            //         break;
            //     default:
            //         break;
            // }
            /* **************** REGISTER COMMAND CALLBACK HERE **************** */
        }
    }

    vTaskDelete(NULL);
}

// app_sr_hanler.h
/*
 * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: CC0-1.0
 */

#pragma once

#include <stdbool.h>
#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Handles speech recognition results
 *
 * This task function waits for speech recognition results from a queue, logs the results, and performs actions based on the results.
 * If a timeout occurs, it simply logs the timeout and continues waiting for the next result.
 * If a wakenet detection occurs, it plays a greeting and continues waiting for the next result.
 * If a multinet detection occurs, it performs an action based on the command ID opening the medicine box
 *
 * @param pvParam A pointer to the queue from which to receive speech recognition results. This should be a `QueueHandle_t`.
 */
void sr_handler_task(void *pvParam);

#ifdef __cplusplus
}
#endif
posted @ 2025-08-15 19:02  小镇青年达师傅  阅读(265)  评论(0)    收藏  举报