第5章:音频处理
欢迎回来!
在探索xiaozhi-esp32
项目的旅程中,我们已经学习了
-
Board概念如何连接
不同硬件
-
Protocol概念如何与
AI服务器通信
-
Application如何担任
系统9种state的指挥者
-
以及Audio Codec如何通过麦克风
获取原始音频数据
并通过扬声器播放声音。
现在想象Application从Audio Codec的麦克风输入接收到一块原始音频数据。这些原始数据并不完美,可能包含以下内容:
- 来自环境的背景噪声
- 当扬声器播放时可能产生的AI语音回声
- 无人说话时的静默片段
直接将这种原始、嘈杂且经常包含静默的数据传递给强大的AI服务器既不高效也不有效。
服务器需要干净的音频,同时明确知道用户*何时*开始或结束说话也非常重要。
此外,设备需要一种方式来判断你何时想要与它对话——例如通过说出特定的"唤醒词"如"你好小智"。
这就是音频处理组件的用武之地。它从Audio Codec获取原始声音数据,在进行Protocol传输和服务器交互之前,执行复杂的分析和清洗
。
什么是音频处理?
音频处理组件就像是麦克风数据的智能过滤器和监听器,其主要职责包括:
- 音频净化:应用以下技术
- 噪声抑制(
NR
):减少干扰
性背景声 - 声学回声消除(
AEC
):从麦克风输入中移除设备自身扬声器的声音(在对话场景中至关重要)
- 噪声抑制(
- 语音活动检测(
VAD
):判断音频流中何时存在真人
语音 - 唤醒词检测(
WWD
):持续监听特定触发短语(如"你好小智
")
该组件的输出不仅包含净化后的音频数据,还会产生以下**事件信号**:
- “语音活动开始!”
- “语音活动停止!”
- “检测到唤醒词’你好小智’!”
这使得Application能智能响应——例如 仅在检测到唤醒词后的语音活动时启动服务器对话
。
音频处理抽象层的使用
Application是音频处理功能的主要使用者,其操作流程为:
- 获取适当的
音频处理
实例 - 将来自Audio Codec的原始音频输入处理器
- 注册回调函数
接收处理结果和事件通知
以下是Application的AudioLoop
与音频处理的交互示例:
void Application::AudioLoop() {
auto codec = Board::GetInstance().GetAudioCodec(); // 获取音频硬件访问
size_t feed_size = 0; // 处理器需要的音频块大小
// 根据处理器需求确定块大小
if (audio_processor_) feed_size = audio_processor_->GetFeedSize();
else if (wake_word_detector_) feed_size = wake_word_detector_->GetFeedSize();
std::vector<int16_t> mic_data(feed_size); // 音频读取缓冲区
while (true) {
// 1. 从麦克风读取原始音频
bool success = codec->InputData(mic_data);
if (success) {
// 2. 输入主音频处理器(AEC/NR/VAD)
if (audio_processor_ && audio_processor_->IsRunning()) {
audio_processor_->Feed(mic_data);
}
// 3. 输入唤醒词检测器
if (wake_word_detector_ && wake_word_detector_->IsDetectionRunning()) {
wake_word_detector_->Feed(mic_data);
}
}
// 注意:处理结果通过回调返回(如下所示)
}
}
回调函数的设置示例(在应用初始化阶段):
// 设置音频处理器回调
if (audio_processor_) {
// 处理完成音频回调
audio_processor_->OnOutput([this](std::vector<int16_t>&& processed_data) {
Schedule([this, data = std::move(processed_data)]() mutable {
// 此处可安排协议层发送数据
});
});
// 语音状态变更回调
audio_processor_->OnVadStateChange([this](bool speaking) {
Schedule([this, speaking]() {
speaking ? ESP_LOGI(TAG, "检测到语音!")
: ESP_LOGI(TAG, "检测到静默!");
});
});
}
// 设置唤醒词检测回调
if (wake_word_detector_) {
wake_word_detector_->OnWakeWordDetected([this](const std::string& wake_word) {
Schedule([this, wake_word]() {
ESP_LOGI(TAG, "唤醒词检测:%s", wake_word.c_str());
StartConversation(wake_word); // 启动对话流程
});
});
}
这是一个音频处理循环的实现,主要完成实时麦克风音频采集
、音频信号处理
(如降噪)和唤醒词检测
功能。
采用模块化设计,通过回调机制异步
处理结果。
音频采集循环详解
AudioLoop()
方法通过以下流程持续工作:
- 创建音频硬件接口实例和数据缓冲区
auto codec = Board::GetInstance().GetAudioCodec();
获取硬件编解码器访问权限std::vector<int16_t> mic_data(feed_size);
创建指定大小的音频缓冲区
根据处理模块需求动态确定数据块大小
优先采用音频处理器的需求尺寸,其次使用唤醒词检测器的需求尺寸,保证数据块符合处理要求
- 持续读取和处理音频数据
codec->InputData(mic_data)
从麦克风同步读取原始音频数据
成功获取数据后,分别Feed
传递给音频处理器和唤醒词检测器模块进行处理
回调机制
处理结果通过预先注册的回调函数异步返回:
音频处理器提供两种回调
OnOutput
返回处理后的音频数据(如降噪后的数据)OnVadStateChange
通知语音活动状态变化(说话/静默)
唤醒词检测器回调
OnWakeWordDetected
在识别到特定唤醒词时触发后续交互
流程- 使用
Schedule
方法将回调任务加入事件队列
,确保线程安全
技术特点
- 硬件抽象层设计:通过
Board
类统一访问音频硬件 - 模块化处理链:音频处理器
audio_processor
和唤醒词检测器wake_word_detector_
可独立配置 - 事件驱动架构:
Schedule
回调 避免阻塞式等待,提高系统响应性 - 线程安全设计:通过任务调度器处理跨线程通信
音频处理内部工作机制
项目采用ESP32专用音频处理库实现核心功能,架构层次如下:
核心接口与实现示例:
// 音频处理器接口定义
class AudioProcessor {
public:
virtual void Initialize(AudioCodec* codec) = 0;
virtual void Feed(const std::vector<int16_t>& data) = 0;
virtual void OnOutput(std::function<void(std::vector<int16_t>&&)> callback) = 0;
// ... 其他方法省略
};
// AFE处理器实现
void AfeAudioProcessor::AudioProcessorTask() {
while (true) {
auto res = afe_iface_->fetch_with_delay(afe_data_, portMAX_DELAY);
if (res->vad_state == VAD_SPEECH) {
vad_state_change_callback_(true); // 触发语音活动
}
output_callback_(processed_audio); // 发送处理后的音频
}
}
音频处理器类型对比
实现类 | 核心功能 | 底层库 | 典型用例 |
---|---|---|---|
AfeAudioProcessor | 降噪 /回声消除/VAD | ESP AFE (语音通信配置) | 语音通信场景 |
DummyAudioProcessor | 直通 模式 | 无 | 测试环境 |
WakeWordDetect | 唤醒词 检测 | ESP-SR WakeNet | 设备激活场景 |
结语
音频处理组件是原始音频与上层系统间的关键桥梁,通过模块化设计和标准接口,使得Application无需关注底层实现细节即可实现复杂的音频处理功能
。
下一章我们将探讨设备的"显示"功能实现。
第6章:显示
欢迎回来!在我们探索xiaozhi-esp32
项目的旅程中,我们已经了解了
应用层如何担任系统指挥者
现在,这个智能AI设备已经能听会说,甚至可以联网对话!但它如何向您展示工作状态?如何显示通知消息或AI的聊天回复?
-
正如不同开发板采用不同音频芯片,它们的显示屏也各不相同。
-
有些可能配备小型单色OLED屏,有些则搭载更大的彩色LCD触摸屏。
-
核心应用层代码无需关心具体屏幕参数(尺寸、色深、SPI/I2C/RGB等接口),只需简单调用"在屏幕上显示’你好!'"。
这正是显示抽象层的价值所在。
什么是显示模块?
xiaozhi-esp32
中的显示抽象层是设备的**标准化屏幕驱动接口**,它隐藏了与具体显示面板硬件交互的底层细节
类比计算机显示驱动:无论使用普通显示器还是电竞屏,浏览器或文本编辑器都无需包含针对具体屏幕的代码。
它们通过操作系统显示驱动接口
工作,由驱动将指令转换为屏幕识别的信号。
可以类比VFS
的设计,加一层实现统一兼容的抽象
显示抽象层提供以下核心功能:
- 状态消息显示(如"连接中…"/“聆听中…”/“待机”)
- 临时通知展示(如"WiFi已连接!")
- 图标动态更新(网络强度、电量、静音状态)
- 情感图标呈现(AI表情反馈)
- 聊天消息展示(AI文字回复)
- 主题切换支持(明/暗模式)
- 硬件无关性:隔离应用层与具体屏幕类型
显示抽象层的使用
应用层通过统一接口更新用户界面,典型调用流程如下:
// 获取当前开发板的显示组件
Board& current_board = Board::GetInstance();
Display* screen = current_board.GetDisplay();
// 调用标准化显示方法
screen->SetStatus("初始化中..."); // 设置主状态
screen->ShowNotification("WiFi已连接", 2000); // 2秒临时通知
screen->SetEmotion("happy"); // 显示笑脸表情
screen->SetChatMessage("assistant", "你好!需要什么帮助?"); // AI回复
screen->SetTheme("dark"); // 切换暗色主题
关键方法说明:
SetStatus()
更新常驻状态栏文字,适用于设备核心状态提示ShowNotification()
短暂弹出式通知,自动超时后恢复原界面SetEmotion()
通过预定义表情符号传递AI情感状态(如"happy"/“thinking”)SetIcon()
使用字体图标库(如Font Awesome)显示系统状态图标SetChatMessage()
结构化展示对话内容,支持角色标识(“user”/“assistant”)SetTheme()
动态切换界面主题,适配不同光照环境
为确保多线程安全,显示模块内置互斥锁机制。通过DisplayLockGuard
类实现资源自动管理:
void Display::SetStatus(const char* status) {
DisplayLockGuard lock(this); // 自动加锁
lv_label_set_text(status_label_, status); // LVGL文本更新
// 函数结束自动释放锁
}
显示模块的技术实现
显示抽象层通过四级架构实现硬件无关性
1. 显示抽象层(display.h)
定义所有显示方法的虚基类,声明LVGL对象指针:
class Display {
protected:
lv_obj_t *status_label_; // 状态标签
lv_obj_t *chat_message_label_; // 聊天消息标签
virtual bool Lock(int timeout) = 0; // 纯虚锁方法
};
为什么设计纯虚锁方法Lock(int timeout)
?
纯虚函数Lock(int timeout)
的设计目的是强制派生类必须实现锁定逻辑
,同时为基类Display
提供统一
的接口规范。
通过抽象锁定行为,不同子类可以自定义锁的实现(如超时处理
、硬件锁
等),而基类无需关心具体细节。
关键设计
接口标准化
Display
作为基类需要确保所有子类具备锁定功能,纯虚函数强制派生类必须实现Lock
,避免遗漏核心功能。例如:
class TouchDisplay : public Display {
bool Lock(int timeout) override {
// 具体实现触摸屏锁定逻辑
}
};
多态性支持
基类指针/引用可以调用子类的Lock
实现,运行时动态绑定。
例如:
Display* disp = new TouchDisplay();
disp->Lock(1000); // 调用TouchDisplay的Lock
灵活扩展
不同设备(如LCD、OLED)的锁定机制可能差异较大,纯虚函数允许各自实现。例如:
class OLEDDisplay : public Display {
bool Lock(int timeout) override {
// OLED特有的锁定逻辑
}
};
⭕设计体现的抽象思想
- 解耦:基类只声明“做什么”(锁定),不定义“怎么做”。
- 可替换性:新增设备类型时只需继承并实现
Lock
,无需修改基类。 - 契约明确:纯虚函数明确告知开发者子类必须完成的功能。
2. 具体实现类
继承基类并实现硬件特定逻辑
class OledDisplay : public Display {
OledDisplay(esp_lcd_panel_handle_t panel) {
// 初始化128x64 OLED的LVGL对象
status_label_ = lv_label_create(lv_scr_act());
}
bool Lock(int timeout) override {
return lvgl_port_lock(timeout);
}
};
3. LVGL图形库
提供跨平台GUI组件,通过"lv_"前缀函数管理界面元素
lv_label_set_text(status_label_, "Ready"); // 更新标签文本
lv_style_set_bg_color(&style, lv_color_hex(0x000000)); // 设置背景色
4. ESP-IDF驱动层
通过esp_lcd
统一管理不同接口屏幕
// 初始化SPI接口的ST7789屏幕
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = GPIO_NUM_5,
.dc_gpio_num = GPIO_NUM_3
};
esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle);
支持的显示类型
实现类 | 接口类型 | 典型分辨率 | 特性 |
---|---|---|---|
OledDisplay | I2C/SPI | 128x64 | 单色OLED,低功耗 |
SpiLcdDisplay | SPI | 240x320 | IPS全彩屏,性价比高 |
RgbLcdDisplay | RGB并行 | 800x480 | 高刷屏,视频播放 |
MipiLcdDisplay | MIPI DSI | 1920x1080 | 手机级屏幕,超薄设计 |
NoDisplay | 无 | - | 无屏设备模拟器 |
开发板通过GetDisplay()
返回适配当前硬件的实例
// boards/ttgo_tdisplay.cpp
Display* TTGO_TDisplay::GetDisplay() {
return new SpiLcdDisplay(panel_io, panel, 240, 320);
}
显示工作流程
当调用SetStatus("待机")
时,系统执行以下硬件交互
结语
显示抽象层是xiaozhi-esp32
项目的关键设计,通过硬件无关接口和LVGL图形库的深度整合,实现了从单色OLED到高清彩屏的统一控制
这种设计模式不仅简化了应用层开发,还为硬件升级提供了无缝扩展能力。
接下来我们将探索设备如何通过"物联网
"模块与其他智能设备交互。