第05章 SR组件
约 3028 字大约 10 分钟
2026-03-23
1. 什么是SR组件
espressif/esp-sr 是乐鑫(Espressif)官方推出的 ESP-SR(Speech Recognition,语音识别)框架,专为在 ESP32 系列芯片上实现 离线、低功耗、高性能的语音交互功能 而设计
ESP-SR 是一个 嵌入式端 AI 语音组件库,让你无需联网即可在 ESP32 设备上实现:
- 唤醒词检测(如“你好小智”)
- 语音命令识别(如“打开灯”、“调高音量”)
- 人声活动检测(VAD)
- 音频前端处理(降噪、回声消除等)
- 声源方向估计(DOA,Direction of Arrival)
所有算法均以 预编译二进制库(.a 文件) + 头文件 的形式提供,不开源模型内部,但可直接集成到 ESP-IDF 项目中
2. 主要模块详解
WakeNet(唤醒词引擎)
功能:持续监听麦克风输入,检测特定唤醒词(如 “Hi, ESP”、“你好小智”)。
特点:
- 超低内存占用(可在无 PSRAM 的 ESP32-C3 上运行,使用
WakeNet9s模型)。 - 支持自定义唤醒词(通过 TTS 合成数据训练)。
- 最新
WakeNet9l针对极快语速优化,WakeNet9s针对低成本芯片优化。
- 超低内存占用(可在无 PSRAM 的 ESP32-C3 上运行,使用
典型应用场景:智能音箱、语音遥控器、IoT 控制面板。
MultiNet(语音命令识别)
功能:识别唤醒后的具体语音指令(如“打开空调”、“播放音乐”)。
支持:
- 中文 / 英文命令
- 最多 300 条自定义命令
- 无需重新训练模型(通过关键词映射实现)
模型示例:
mn7_cn:中文,高精度,需 PSRAM(推荐 ESP32-S3)mn5q8_en:英文,量化压缩版,适合资源受限设备
AFE(Audio Front-End,音频前端)
功能:提升语音输入质量,包含:
- AEC(回声消除):消除扬声器播放声音对麦克风的干扰
- NS / NSNET(噪声抑制):抑制环境噪音(风扇、交通声等)
- BSS(盲源分离):多麦克风波束成形(需 2 麦克风)
- VAD(语音活动检测):判断是否有有效人声
已通过 Amazon Alexa Built-in 认证(双麦方案)
VADNet(新一代语音活动检测)
- 替代传统的 WebRTC VAD
- 基于深度学习,更准确识别真实人声 vs 噪音
- 减少误触发,提升唤醒体验
DOA(声源方向估计)
- 新增于 2025 年 4 月
- 利用多麦克风阵列估算说话人方向
- 可用于机器人转头、摄像头跟踪等场景
3 安装使用
3.1 下载
官方组件库中搜索 esp-sr,获取下载指令
idf.py add-dependency "espressif/esp-sr^2.4.0"3.2 官方文档
语音识别流程:

语音通话流程:

3.3 配置
3.3.1 更新分区表
您必须添加一个 partition.csv 文件,并确保有足够的空间来存储所选的模型。 在项目的 partitions.csv 文件中添加以下行,以分配模型所需的空间:
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
ota_0, app, ota_0, , 6500K,
ota_1, app, ota_1, , 500K,
model, data, , , 500Kpartition.csv 格式解读:
Name, Type, SubType, Offset, Size| 列名 | 说明 | 示例 |
|---|---|---|
| Name | 分区名称(字符串),在代码中引用 | phy_init, nvs |
| Type | 分区类型(字符串或十六进制) | app(0x00), data(0x01) |
| SubType | 子类型(字符串或十六进制) | factory(0x00), ota_0(0x10) |
| Offset | 起始地址(十进制或十六进制) | 0x10000, partition_size |
| Size | 分区大小(支持单位) | 0x1000, 1M, 512K |
3.3.3 修改配置使分区表生效

3.3.3 配置唤醒词

4. 代码实现
4.1 创建SR实例
audio_sr.h
#ifndef __AUDIO_SR_H__
#define __AUDIO_SR_H__
#include "esp_heap_caps.h"
// 定义语音识别结构体类型的别名
typedef struct audio_sr_struct audio_sr_t;
/**
* @brief 创建SR对象
*
* @return audio_sr_t* SR对象(结构体指针)
*/
audio_sr_t *audio_sr_create(void);
#endif /* __AUDIO_SR_H__ */audio_sr.c
#include "audio_sr.h"
// 定义语音识别的结构体类型
struct audio_sr_struct {
};
/**
* @brief 创建SR对象
*
* @return audio_sr_t* SR对象(结构体指针)
*/
audio_sr_t *audio_sr_create(void)
{
// 分配存储SR对象的内存空间,放在PSRAM中
audio_sr_t *audio_sr = (audio_sr_t *)heap_caps_malloc(sizeof(audio_sr_t), MALLOC_CAP_SPIRAM);
// 返回
return audio_sr;
}4.2 语音检测过程中的回调函数
audio_sr.h
/* 代码省略... */
/**
* @brief 注册VAD回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param vad_cb 回调函数
*/
void audio_sr_register_vad_cb(audio_sr_t *audio_sr, void (*vad_cb)(void));
/**
* @brief 注册WakeUp回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param wakeup_cb 回调函数
*/
void audio_sr_register_wakeup_cb(audio_sr_t *audio_sr, void (*wakeup_cb)(void));audio_sr.c
/* 代码省略... */
// 定义语音识别的结构体类型
struct audio_sr_struct {
// VAD 检测到语音活动调用的回调函数
void (*vad_cb)(void);
// WakeUp 检测到唤醒词调用的回调函数
void (*wakeup_cb)(void);
};
/* 代码省略... */
/**
* @brief 注册VAD回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param vad_cb 回调函数
*/
void audio_sr_register_vad_cb(audio_sr_t *audio_sr, void (*vad_cb)(void))
{
audio_sr->vad_cb = vad_cb;
}
/**
* @brief 注册WakeUp回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param wakeup_cb 回调函数
*/
void audio_sr_register_wakeup_cb(audio_sr_t *audio_sr, void (*wakeup_cb)(void))
{
audio_sr->wakeup_cb = wakeup_cb;
}4.3 添加环形缓冲区存储处理好的音频数据
audio_sr.h
#include "freertos/ringbuf.h"
/* 代码省略... */
/**
* @brief 注册环形缓冲区
*
* @param audio_sr SR对象(结构体指针)
* @param ringbuf 环形缓冲区句柄
*/
void audio_sr_register_ringbuf(audio_sr_t *audio_sr, RingbufHandle_t ringbuf);audio.sr.c
// 定义语音识别的结构体类型
struct audio_sr_struct {
/* 代码省略... */
// 环形缓冲区的句柄,存放经过SR处理过的数据
RingbufHandle_t ringbuf;
};
/* 代码省略... */
/**
* @brief 注册环形缓冲区
*
* @param audio_sr SR对象(结构体指针)
* @param ringbuf 环形缓冲区句柄
*/
void audio_sr_register_ringbuf(audio_sr_t *audio_sr, RingbufHandle_t ringbuf)
{
audio_sr->ringbuf = ringbuf;
}4.3 初始化AFE配置和创建AFE实例
audio_sr.h
#include "esp_afe_sr_iface.h"
#include "esp_afe_sr_models.h"
/* 代码省略... */
/**
* @brief 启动SR组件
*
* @param audio_sr SR实例
*/
void audio_sr_start(audio_sr_t *audio_sr);
/* 代码省略... */audio_sr.c
// 定义语音识别的结构体类型
struct audio_sr_struct
{
// AFE 句柄
esp_afe_sr_iface_t *afe_handle;
// AFE 实例
esp_afe_sr_data_t *afe_data;
/* 代码省略... */
};
/**
* @brief 启动SR组件
*
* @param audio_sr SR实例
*/
void audio_sr_start(audio_sr_t *audio_sr)
{
// 1. 初始化AFE配置
// 1.1 初始化模型列表,参数是模型分区名名
srmodel_list_t *models = esp_srmodel_init("model");
// 1.2 初始化AFE配置结构体 参数:通道排列;模型列表(上一步的结果);AFE类型(语音识别);AFE模式(高性能)
afe_config_t *afe_config = afe_config_init("M", models, AFE_TYPE_SR, AFE_MODE_HIGH_PERF);
// 1.3 设置其他配置项: 关闭不需要的模块
afe_config->aec_init = false; // 不开启回声消除
afe_config->se_init = false; // 不开启语音增强
afe_config->ns_init = false; // 不开启噪声抑制
afe_config->agc_init = false; // 不开启自动增益控制
// 1.4 设置其他配置项: VAD 设置
afe_config->vad_init = true; // 开启语音活动检测
afe_config->vad_mode = VAD_MODE_3; // 设置VAD模式为模式3,性能和功耗适中
// 1.5 设置其他配置项: 唤醒词设置
afe_config->wakenet_init = true; // 开启唤醒词检测
afe_config->wakenet_mode = DET_MODE_95; // 激进模式
// 1.6 设置其他配置项: 通用设置
afe_config->pcm_config.total_ch_num = 1; // 输入音频总通道数
afe_config->pcm_config.mic_num = 1; // 麦克风通道数
afe_config->pcm_config.sample_rate = 16000; // 输入音频采样率
afe_config->memory_alloc_mode = AFE_MEMORY_ALLOC_MORE_PSRAM; // PSRAM
// 2. 创建 AFE 实例
// 2.1 使用afe_config获取AFE句柄, 将句柄放入SR对象中
audio_sr->afe_handle = esp_afe_handle_from_config(afe_config);
// 2.2 创建实例,将AFE实例放入SR对象中
audio_sr->afe_data = audio_sr->afe_handle->create_from_config(afe_config);
}
/* 代码省略... */4.4 输入ES8311获取的音频数据
audio.h
#include "int_es8311.h"audio.c
/* 代码省略... */
// 定义语音识别的结构体类型
struct audio_sr_struct
{
/* 代码省略... */
// AFE 输入缓冲区的大小
size_t afe_buffer_size;
/* 代码省略... */
};
// 静态函数:作为给AFE喂数据的任务函数
static void audio_sr_afe_feed_func(void *arg)
{
// 获取参数
audio_sr_t *audio_sr = (audio_sr_t *)arg;
// 3. 输入音频数据(音频数据来自于ES8311,获取与麦克风)
// 3.1 获取AFE每次需要处理的音频帧大小(采样点数量)
int feed_chunksize = audio_sr->afe_handle->get_feed_chunksize(audio_sr->afe_data);
// 3.2 获取AFE输入的音频通道数
int feed_nch = audio_sr->afe_handle->get_feed_channel_num(audio_sr->afe_data);
// 3.4 计算缓冲区的大小
audio_sr->afe_buffer_size = feed_chunksize * feed_nch * sizeof(int16_t);
// 3.3 为AFE输入数据分配缓冲区, 将缓冲区放入PSRAM中
int16_t *feed_buff = (int16_t *)heap_caps_malloc(audio_sr->afe_buffer_size, MALLOC_CAP_SPIRAM);
// 3.4 将音频数据喂给AFE进行处理
while (1)
{
// 获取到ES8311的麦克风数据,存入feed_buff中
int_es8311_read_mic((uint8_t *)feed_buff, audio_sr->afe_buffer_size);
// 将数据喂给AFE
audio_sr->afe_handle->feed(audio_sr->afe_data, feed_buff);
}
}
/* 代码省略... */
/**
* @brief 启动SR组件
*
* @param audio_sr SR实例
*/
void audio_sr_start(audio_sr_t *audio_sr)
{
/* 前两步: 1. 初始化AFE配置 2.创建 AFE 实例 代码省略 ...*/
// 3. 创建RTOS任务,不断给AFE喂数据(音频数据来自于ES8311,获取于麦克风)(任务栈放在PSRAM中)
xTaskCreateWithCaps(audio_sr_afe_feed_func, "afe_feed_task", 32 * 1024, audio_sr, 10, NULL, MALLOC_CAP_SPIRAM);
}4.5 获取AFE处理结果
audio_sr.h
// 代码省略 ...
// vad_cb 函数的参数修改,注册回调函数的函数也要修改
/**
* @brief 注册VAD回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param vad_cb 回调函数
*/
void audio_sr_register_vad_cb(audio_sr_t *audio_sr, void (*vad_cb)(vad_state_t));
/**
* @brief 关闭SR唤醒
*
* @param audio_sr SR对象(结构体指针)
*/
void audio_sr_stop_wakeup(audio_sr_t *audio_sr);
// 代码省略 ...audio_sr.c
// 定义结构体类型
struct audio_sr_struct
{
// 修改
// 检测到语音活动的回调函数
void (*vad_cb)(vad_state_t);
/* 代码省略... */
// 添加如下
// 是否被唤醒的标志位
bool is_wakeup;
// 上一次的检测人声的状态
vad_state_t vad_state_last;
};
// 静态函数:作为从AFE中获取数据的任务的函数
static void audio_sr_afe_fetch_func(void *arg)
{
// 获取参数
audio_sr_t *audio_sr = (audio_sr_t *)arg;
while (1)
{
// 4.1 从AFE获取处理后的音频帧和检测结果
afe_fetch_result_t *result = audio_sr->afe_handle->fetch(audio_sr->afe_data);
// 4.2 获取处理后的音频数据指针
int16_t *processed_audio = result->data;
// 4.3 获取VAD(语音活动检测)状态
vad_state_t vad_state = result->vad_state;
// 4.4 获取唤醒词检测状态
wakenet_state_t wakeup_state = result->wakeup_state;
// 判断VAD状态发生变化
if (vad_state != audio_sr->last_vad_state)
{
// 调用回调函数,并传入当前的vad状态
audio_sr->vad_cb(vad_state);
// 修改 last_vad_state
audio_sr->last_vad_state = vad_state;
}
// 判断检测到唤醒词
if (wakeup_state == WAKENET_DETECTED)
{
// 修改唤醒状态
audio_sr->is_wakeup = true;
// 调用回调函数
audio_sr->wakeup_cb();
}
// 当vad检测到声音且唤醒,将获取的AFE结果放入环形缓冲区
if (vad_state == VAD_SPEECH && audio_sr->is_wakeup)
{
xRingbufferSend(audio_sr->ringbuf, processed_audio, audio_sr->afe_buffer_size, portMAX_DELAY);
}
}
}
/**
* @brief 创建SR对象
*
* @return audio_sr_t* SR对象(结构体指针)
*/
audio_sr_t *audio_sr_create(void)
{
/* 代码省略... */
// 给audio_sr 的成员设置默认值
audio_sr->last_vad_state = VAD_SILENCE;
audio_sr->is_wakeup = false;
// 返回
return audio_sr;
}
void audio_sr_start(audio_sr_t *audio_sr)
{
/* 代码省略... */
// 4. 创建RTOS任务,不断从AFE中获取处理结果(任务栈放入PSRAM)
xTaskCreateWithCaps(audio_sr_afe_fetch_func, "afe_fetch_task", 32 * 1024, audio_sr, 10, NULL, MALLOC_CAP_SPIRAM);
}
// 修改回调函数的参数
/**
* @brief 注册VAD回调函数
*
* @param audio_sr SR对象(结构体指针)
* @param vad_cb 回调函数
*/
void audio_sr_register_vad_cb(audio_sr_t *audio_sr, void (*vad_cb)(vad_state_t))
{
audio_sr->vad_cb = vad_cb;
}
/**
* @brief 关闭SR唤醒
*
* @param audio_sr SR对象(结构体指针)
*/
void audio_sr_stop_wakeup(audio_sr_t *audio_sr)
{
audio_sr->is_wakeup = false;
}
/* 代码省略... */4.6 测试
main.c
#include <stdio.h>
#include "esp_task.h"
#include "com_debug.h"
#include "int_es8311.h"
#include "audio_sr.h"
// 定义声音活动检测的回调函数
void app_main_vad_cb(vad_state_t vad_state)
{
if (vad_state == VAD_SPEECH)
{
MY_LOGI("检测到人声!\n");
}
else
{
MY_LOGI("检测到静音!\n");
}
}
// 定义唤醒后调用的回调函数
void app_main_wakeup_cb(void)
{
MY_LOGI("AI小智被唤醒!");
}
void app_main(void)
{
MY_LOGI("ES8311 Test:");
// 初始化 ES8311 音频编解码器
int_es8311_init();
// 创建SR对象
audio_sr_t *audio_sr = audio_sr_create();
// 给SR对象注册回调函数
audio_sr_register_vad_cb(audio_sr, app_main_vad_cb);
audio_sr_register_wakeup_cb(audio_sr, app_main_wakeup_cb);
// 创建环形缓冲区并添加到SR对象中
RingbufHandle_t ringbuf = xRingbufferCreateWithCaps(64 * 1024, RINGBUF_TYPE_NOSPLIT, MALLOC_CAP_SPIRAM);
audio_sr_register_ringbuf(audio_sr, ringbuf);
// 启动语音检测
audio_sr_start(audio_sr);
// 定义变量,保存从环形缓冲区获取到的数据长度
size_t len = 0;
// 定义变量,保存从环形缓冲区获取到的数据
void *data = NULL;
while (1)
{
// 从环形缓冲区获取数据, 如果获取不到会阻塞
data = xRingbufferReceive(ringbuf, &len, portMAX_DELAY);
// 将读取到的数据写入扬声器
int_es8311_write_speaker(data, len);
// 释放环形缓冲区资源
vRingbufferReturnItem(ringbuf, data);
}
}