[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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值