[xiaozhi-esp32] 应用层(9种state) | 音频编解码层 | 双循环架构

第三章:应用层

在第一章:开发板抽象层中,我们实现了硬件交互标准化;在第二章:通信协议层中,我们构建了云端通信桥梁

现在需要将这些能力有机整合——这便是应用层的使命

应用层的本质

应用层是设备的中枢神经系统,承担以下核心职责:

  1. 状态管理
    维护设备运行状态机(空闲、连接中、监听、播报等9种状态),协调各模块工作流程
  2. 事件调度
    通过双循环架构(主事件循环+音频循环)实现多线程安全调度
  3. 组件编排
    调用开发板组件实现人机交互,驱动协议层进行云端通信
  4. 异常处理
    监控系统运行状态,处理网络中断、硬件故障等异常情况

类比交响乐团指挥:应用层 不直接演奏乐器(硬件操作)或翻译乐谱(协议转换),而是统筹全局节奏与声部配合

EventLoop

程序需要同时处理多个任务(比如用户输入、网络请求),但CPU一次只能做一件事。Event Loop就像餐厅服务员,轮询检查哪些任务准备好了(比如菜做好了),按顺序处理避免阻塞等待,让程序高效响应。

应用层启动流程

系统入口app_main初始化应用实例:

// 文件:main/main.cc(简化版)
extern "C" void app_main(void) 
{esp_event_loop_create_default(); // 创建ESP32事件循环nvs_flash_init();                // 初始化非易失存储Application::GetInstance().Start(); // 启动应用层主控
}

Application::Start()初始化流程:

// 文件:main/application.cc(节选)
void Application::Start() {auto& board = Board::GetInstance(); // 获取开发板实例display_ = board.GetDisplay();       // 初始化显示屏audio_codec_ = board.GetAudioCodec(); // 获取音频编解码器InitNetworkConnection();   // 启动网络模块protocol_ = CreateProtocol(); // 根据配置创建协议实例// 注册协议层回调protocol_->OnIncomingJson(HandleServerMessage);protocol_->OnAudioReceived(HandleAudioPacket);xTaskCreate(MainEventLoop, "MainLoop", 4096, this, 5, NULL); // 创建主事件循环任务xTaskCreate(AudioLoop, "AudioLoop", 4096, this, 6, NULL);     // 创建音频处理任务
}

状态机设计与实现

应用层通过枚举类型管理9大设备状态

// 文件:main/application.h(状态机定义)
enum DeviceState {kDeviceStateUnknown,       // 未知状态kDeviceStateStarting,      // 启动初始化kDeviceStateWifiConfiguring, // WiFi配置中kDeviceStateIdle,          // 空闲待命kDeviceStateConnecting,    // 服务器连接中kDeviceStateListening,     // 语音采集状态kDeviceStateSpeaking,      // 语音播报状态,后面会以这个为例kDeviceStateUpgrading,     // 固件升级中kDeviceStateFatalError     // 严重错误状态
};

状态切换触发对应硬件操作:
在这里插入图片描述

双循环任务架构

主事件循环(MainEventLoop)

void Application::MainEventLoop() {while (true) {xEventGroupWaitBits(event_group_, EVENT_FLAG, pdTRUE, pdFALSE, portMAX_DELAY);std::unique_lock<std::mutex> lock(mutex_);auto tasks = std::move(main_tasks_); // 获取待处理任务for (auto& task : tasks) {task(); // 执行状态变更、UI更新等核心操作}}
}

通过Schedule()实现跨线程安全调用:

// 网络回调线程
void OnNetworkConnected() {Application::GetInstance().Schedule([](){app.SetDeviceState(kDeviceStateIdle);display.ShowStatus("准备就绪");});
}

unique_lock

unique_lock是C++标准库中提供的一种灵活的互斥量所有权管理工具,属于<mutex>头文件。

它比lock_guard更强大,允许延迟锁定、条件变量配合以及手动解锁
unique_lock独占互斥量的所有权(不可复制),但支持移动语义(所有权转移)。

典型场景包括需要灵活控制锁的粒度,或在条件变量等待时自动释放锁。

示例代码:

std::mutex mtx;
{std::unique_lock<std::mutex> lock(mtx); // 自动锁定// 临界区操作...lock.unlock(); // 可手动提前解锁// 非临界区操作...lock.lock(); // 再次锁定
} // 离开作用域自动解锁
move

move是C++11引入的关键字,用于触发移动语义

它将对象标记为“可移动的”,允许资源(如堆内存)的所有权转移而非复制,避免不必要的深度拷贝。
被移动后的源对象处于有效但不确定状态(通常为空或默认状态)。

移动语义对管理大型资源(如动态数组文件handle)的性能优化至关重要。

示例代码:

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); 
// v1现在为空,v2接管了原数据

两者联系:unique_lock的实现依赖move移动语义,因为互斥量所有权不可复制但可转移。


音频处理循环(AudioLoop)

void Application::AudioLoop() {auto codec = Board::GetInstance().GetAudioCodec();while (true) {// 输入处理:16kHz 16bit PCM采样auto input = codec->ReadMic(1024); audio_processor_.FeedData(input);// 输出处理:OPUS解码与播放if (!audio_queue_.empty()) {auto pkt = audio_queue_.pop();auto pcm = opus_decoder_.Decode(pkt);codec->WriteSpeaker(pcm);}vTaskDelay(1); // 释放CPU}
}

⭕协议交互实现

应用层处理服务器消息的典型流程:

void Application::HandleServerMessage(cJSON* root) 
{auto type = cJSON_GetObjectItem(root, "type");if (strcmp(type->valuestring, "tts") == 0) { // 语音合成指令auto text = cJSON_GetObjectItem(root, "text");Schedule([=]() {if (device_state_ == kDeviceStateListening) {tts_engine.Synthesize(text->valuestring);SetDeviceState(kDeviceStateSpeaking); // 状态切换}});}// 处理其他指令类型:stt/iot/alert等cJSON_Delete(root); // 释放JSON内存
}

这段代码是一个服务器消息处理函数,负责解析JSON格式的指令并执行相应操作。

收到语音合成指令时,会触发文本转语音功能。

解析
auto type = cJSON_GetObjectItem(root, "type");

从JSON数据中提取"type"字段,判断指令类型。JSON是现代网络通信中常用的轻量级数据格式。

if (strcmp(type->valuestring, "tts") == 0)

检查是否为文本转语音(TTS)指令。"tts"是Text-To-Speech的缩写,表示需要将文字转为语音输出

auto text = cJSON_GetObjectItem(root, "text");
Schedule([=]() {if (device_state_ == kDeviceStateListening) {tts_engine.Synthesize(text->valuestring);SetDeviceState(kDeviceStateSpeaking);}
});

获取要合成的文本内容,在设备处于监听状态时,通过TTS引擎合成语音并将设备状态切换为说话状态
使用lambda表达式延迟执行,避免阻塞主线程。

cJSON_Delete(root);

最后释放JSON对象占用的内存,防止内存泄漏。这是C语言中手动内存管理的典型操作。

应用场景

这类代码常见于智能语音设备(如智能音箱)中,当服务器需要设备播报内容时,会发送包含"type":"tts"和"text":"要播报的内容"的JSON指令

设备收到后就会用合成语音读出指定文字


核心类结构

Application类主要成员:

class Application {
private:Board& board_;                  // 开发板引用Protocol* protocol_;            // 协议实例Display* display_;              // 显示组件DeviceState device_state_;      // 当前设备状态std::list<std::function<void()>> main_tasks_; // 任务队列Queue<AudioPacket> audio_queue_; // 音频数据队列public:static Application& GetInstance(); // 单例访问void Start();                     // 系统启动入口void Schedule(std::function<void()> task); // 任务调度// 状态控制方法void SetDeviceState(DeviceState state);void StartListening();void StopListening();// 音频控制void PlayAudio(const AudioPacket& pkt);void StopAudio();
};

异常处理机制

应用层通过状态监控实现故障恢复:

void Application::MonitorSystem() {if (protocol_->GetConnectionStatus() == kDisconnected) {SetDeviceState(kDeviceStateConnecting);AttemptReconnect();}if (audio_codec_->CheckHardwareError()) {SetDeviceState(kDeviceStateFatalError);display.ShowAlert("音频硬件故障");}
}

结语

应用层作为xiaozhi-esp32项目的智能中枢,通过三大创新设计实现稳定运行:

  1. 分层状态机:9种状态精准管控设备生命周期
  2. 双循环架构主事件循环(10ms粒度)与音频循环(1ms粒度)分离保障实时性
  3. 安全调度机制跨线程任务队列+互斥锁避免资源竞争

下一章将深入解析实现高质量语音交互的基石——音频编解码层。


第四章:音频编解码层

在第一章:开发板抽象层中,我们实现了硬件交互标准化

在第二章:通信协议层中,我们构建了云端通信桥梁;

在第三章:应用层中,我们建立了智能中枢(通过九种状态的切换)。

本章将深入解析实现高质量语音交互的基石——音频编解码层

本质

音频编解码层是设备的声音处理中枢,承担以下核心职能:

  1. 硬件抽象
    统一不同音频芯片(ES8388/ES8311等)和接口(I2S/PCM)的操作方式
  2. 数据通路
    提供标准化的麦克风数据采集与扬声器播放接口
  3. 状态管理
    控制音频输入/输出的启停状态与音量调节

类比计算机的声卡驱动:应用层无需知晓底层是集成声卡还是外置USB声卡,只需调用统一API

核心接口与使用范式

通过开发板抽象层获取音频组件实例:

// 获取当前开发板适配器
Board& current_board = Board::GetInstance();// 获取音频编解码器实例
AudioCodec* audio_hardware = current_board.GetAudioCodec();

基础操作接口

// 启用麦克风输入
audio_hardware->EnableInput(true); // 读取16kHz 16bit PCM数据(10ms片段)
std::vector<int16_t> buffer(160); // 160 samples = 16000Hz * 0.01s
audio_hardware->InputData(buffer);// 启用扬声器输出 
audio_hardware->EnableOutput(true);// 播放解码后的音频数据
std::vector<int16_t> pcm_data = DecodeOpusPacket(opus_packet);
audio_hardware->OutputData(pcm_data);// 设置输出音量(0-100线性调节)
audio_hardware->SetOutputVolume(75);

内部实现机制

音频编解码层通过继承体系实现多态,架构如下:

基类定义(audio_codec.h)

class AudioCodec {
public:virtual ~AudioCodec();// 标准接口virtual void EnableInput(bool) = 0;virtual void EnableOutput(bool) = 0;virtual bool InputData(std::vector<int16_t>&) = 0;virtual void OutputData(std::vector<int16_t>&) = 0;virtual void SetOutputVolume(int) = 0;protected:// 硬件级读写接口virtual int Read(int16_t* buffer, int samples) = 0;virtual int Write(const int16_t* data, int samples) = 0;// I2S通道句柄i2s_chan_handle_t tx_handle_;i2s_chan_handle_t rx_handle_;
};

具体实现示例

直连I2S方案(NoAudioCodec)
class NoAudioCodec : public AudioCodec {int Read(int16_t* dest, int samples) override {size_t bytes_read;i2s_channel_read(rx_handle_, dest, samples*2, &bytes_read, portMAX_DELAY);return bytes_read / 2; // 返回实际采样数}int Write(const int16_t* data, int samples) override {size_t bytes_written;i2s_channel_write(tx_handle_, data, samples*2, &bytes_written, portMAX_DELAY);return bytes_written / 2;}
};
ES8388芯片方案
class Es8388AudioCodec : public AudioCodec {int Read(int16_t* dest, int samples) override {esp_codec_dev_read(input_dev_, dest, samples*2); // 通过codec库读取return samples;}int Write(const int16_t* data, int samples) override {esp_codec_dev_write(output_dev_, data, samples*2);return samples;}private:esp_codec_dev_handle_t input_dev_;  // 输入设备句柄esp_codec_dev_handle_t output_dev_; // 输出设备句柄
};

数据流可视化

在这里插入图片描述

支持硬件类型对比

实现类适用硬件核心特性
NoAudioCodec直连I2S麦克风/扬声器直接调用ESP-IDF I2S API
Es8388AudioCodecES8388编解码芯片使用esp_codec_dev库,支持I2C配置
BoxAudioCodecES8311+ES7210组合方案输入输出独立控制,支持TDM模式
Es8311AudioCodecES8311低功耗芯片优化功耗管理,支持深度睡眠唤醒

结语

音频编解码层通过三大创新设计实现跨平台兼容:

  1. 双缓冲机制:应用层环形缓冲区与DMA直通缓冲区分离,降低延迟
  2. 动态采样率适配:自动识别16kHz/48kHz等采样率,支持实时重采样
  3. 硬件状态监控:实时检测麦克风断线、扬声器过载等异常

下一章将深入语音交互的核心——音频处理模块,解析如何从原始PCM数据中提取有效语音

之前也有讲过html种有效数据的提取,在制作boost引擎 | 数据清洗当中,相关前文:
[项目详解][boost搜索引擎#1] 概述 | 去标签 | 数据清洗 | scp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/84713.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/84713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 锁升级的过程详解

Java 锁升级的过程详解 Java 虚拟机(JVM)为了提高多线程并发的效率,对内置锁(synchronized 关键字)的实现进行了一系列优化。这些优化体现在锁的升级过程中,即当竞争程度从低到高变化时,锁的状态会从偏向锁逐渐升级为轻量级锁,最终升级为重量级锁。这个过程是不可逆的…

使用vitis tcl脚本构建vitis app工程

一&#xff1a;最近重新学习了zynq系列开发&#xff0c;想着使用tcl创建工程&#xff0c;因此分享一下脚本例子 #!/bin/bashsource /tools/Xilinx/Vitis/2022.2/settings64.sh cd ../../ . ./script/project.sh cd app/script #tcl脚本只能在虚拟机桌面执行 xsct build_vitis…

电脑商城--购物车

加入购物车 1 购物车-创建数据表 1.使用use命令先选中store数据库。 USE store; 2.在store数据库中创建t_cart用户数据表。 CREATE TABLE t_cart (cid INT AUTO_INCREMENT COMMENT 购物车数据id,uid INT NOT NULL COMMENT 用户id,pid INT NOT NULL COMMENT 商品id,price BIG…

2024-2025学年度下期《网页设计》期末模拟测试

一、 单选题 1. HTML文档的根标签是( ) A. <html> B. <head> C. <body> D. <!DOCTYPE> 2. 用于定义段落内容的标签是&#xff1a;( ) A. <div> B. <p> C. <span> D. <br> 3. 网以下哪个属性用于定义CSS内联样式…

搭建加解密网站遇到的问

本机向云服务器传输文件 用winscp 服务器在安装 SSH 服务时自动生成密钥对&#xff08;公钥私钥&#xff09; 为什么要有指纹验证&#xff1f; 防止中间人攻击&#xff08;Man-in-the-Middle&#xff09; 指纹验证打破这个攻击链&#xff1a; 小问题 安装python时 ./confi…

CSS 制作学成在线网页

1 项目结构 1.1 总结 2 网页制作思路 3 header 区域 - 布局 3.1 通栏 3.2 logo 3.3 导航 3.4 搜索区域 3.5 用户区域 4 banner 区域 4.1 左侧侧导航 4.2 右侧课程表 5 精品推荐 6 推荐课程区域 参考链接&#xff1a; 82-准备工作-项目目录与版心_哔哩哔哩_bilibili

图灵完备之路(数电学习三分钟)----门的多路化

上一章中我们学习了如何用与非门实现其他逻辑门&#xff0c;但上节中的输入信号始终为2&#xff0c;但在现实中&#xff0c;输入的信号数量是不确定的&#xff0c;所以我们需要设计多输入的门&#xff1a; 1.三路与非门&#xff08;卡诺图法&#xff09; 我们还是从与非门开始…

【前端】二进制文件流下载(get、post)再谈一次

最近二进制文件流下载可谓是又出幺蛾子&#xff0c;翻阅以前的文章也找不到解决方案&#xff0c;感觉还是没用完全理解&#xff0c;这次再整理一遍。 先说一个通用场景&#xff0c;就是无论get还是post在接口请求的时候设定好 headers: { Content-Type: application/json;cha…

uv功能介绍和完整使用示例总结

以下是关于 UV 工具的完整使用示例总结,结合其核心功能与典型场景,帮助用户快速上手并高效管理 Python 项目: 一、安装与配置 快速安装 macOS/Linux:curl -LsSf https://astral.sh/uv/install.sh | shWindows:powershell -ExecutionPolicy ByPass -c "irm https://as…

MySQL启动报错“mysqld_safe Directory ‘/var/lib/mysql‘ don‘t exists“终极解决方案!从入门到高阶全攻略

在MySQL的使用过程中&#xff0c;启动报错mysqld_safe Directory /var/lib/mysql dont exists是开发者经常遇到的问题。这个错误看似简单&#xff0c;实则可能涉及目录权限、系统配置、文件系统等多个方面。本文将结合官方文档与实际经验&#xff0c;从基础到高级&#xff0c;为…

python 常见数学公式函数使用详解

Python 数学公式与函数大全 Python 提供了丰富的数学计算支持&#xff0c;包括内置函数、标准库&#xff08;math、cmath、numpy&#xff09;和第三方库&#xff08;sympy、scipy&#xff09;。以下是常用数学公式和函数的分类整理&#xff1a; 1. 基本数学运算 1.1 算术运算…

阿里云服务器+宝塔面板发布网站

一、租用服务器 &#xff08;1&#xff09;、进入官网 阿里云-计算&#xff0c;为了无法计算的价值阿里云——阿里巴巴集团旗下公司&#xff0c;是全球领先的云计算及人工智能科技公司之一。提供免费试用、云服务器、云数据库、云安全、云企业应用等云计算服务&#xff0c;以…

langchain框架中各种Agent(LLMSingleAgent ReactAgent Plan-and-Execute Agent)原理方式对比

在LangChain框架中&#xff0c;LLMSingleActionAgent与ReAct Agent及其他Agent类型在内部原理上存在显著差异&#xff0c;主要体现在推理机制、行动策略、动态性等方面。以下结合实例进行详细说明&#xff1a; 1. LLMSingleActionAgent的内部原理 LLMSingleActionAgent是LangC…

AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年6月22日第116弹

从今天开始&#xff0c;咱们还是暂时基于旧的模型进行预测&#xff0c;好了&#xff0c;废话不多说&#xff0c;按照老办法&#xff0c;重点8-9码定位&#xff0c;配合三胆下1或下2&#xff0c;杀1-2个和尾&#xff0c;再杀4-5个和值&#xff0c;可以做到100-300注左右。 (1)定…

电池模块仿真 - 线性时不变降阶模型

电池模块热设计挑战 针对使用周期设计电池模块存在几个独特的热工程挑战。 使用循环&#xff08;例如驾驶循环&#xff09;涉及可变的负载、速度和环境条件&#xff0c;要求电池在动态压力下提供一致的性能。管理热行为至关重要&#xff0c;因为波动的电流会产生热量&#xf…

408第二季 - 组成原理 - IO方式II

继续中断 中断优先级包括响应优先级和处理优先级 注意下面的&#xff0c;很多都是之前说的 这里的中断向量的地址&#xff0c;就是下面的很粗的箭头 一个很复杂的图 然后记一下很复杂的东西 关中断&#xff0c;保存断点和中断服务程序寻址都是之前讲过的 继续推进&#xff01;…

Spring AOP:横切关注点的优雅解决方案

目录 概要 和面向对象编程的区别 优点 AOP的底层原理 JDK动态代理技术 AOP七大术语 切点表达式 AOP实现方式 Spring对AOP的实现包括以下3种方式&#xff1a; 在本篇文章中&#xff0c;我们主要讲解前两种方式。 基于AspectJ的AOP注解式开发 定义目标类以及目标方法…

开源 Arkts 鸿蒙应用 开发(三)Arkts语言的介绍

文章的目的为了记录使用Arkts 进行Harmony app 开发学习的经历。本职为嵌入式软件开发&#xff0c;公司安排开发app&#xff0c;临时学习&#xff0c;完成app的开发。开发流程和要点有些记忆模糊&#xff0c;赶紧记录&#xff0c;防止忘记。 相关链接&#xff1a; 开源 Arkts …

hot100 -- 16.多维动态规划

1.不同路径 问题&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条…

优先级继承和优先级天花板(pthread_mutexattr_setprotocol)

优先级继承和优先级天花板&#xff0c;均可以解决优先级翻转问题。 优先级翻转&#xff1a; 实例观察优先级翻转和优先级继承现象-CSDN博客 如果有两个线程A和B&#xff0c;A的优先级大于B的优先级。在B获取锁之后&#xff0c;释放锁之前&#xff0c;A想要获取锁&#xff0c…