音频代码重构

一、代码结构更改

1.目前代码结构

在这里插入图片描述
在这里插入图片描述

音频输入和音频输出作为两个独立的组件,既然音频输入和输出在很多场合都是同时出现的,
我们可以将这两个组件封装起来成为一个音频组件,乐鑫已经将这件事做好了
那就是 esp_codec_dev 组件

2.使用esp_codec_dev 组件框架

https://github.com/espressif/esp-adf/blob/60054d26e02b358de2ac3db0d2b631a46ae240b8/components/esp_codec_dev/README_CN.md
网站上有各种介绍
在这里插入图片描述
我们的结构简洁来说就是
在这里插入图片描述

3.整体框架

在这里插入图片描述
留个印象,后面写代码具体理解。

二、代码整体框架

1、创建音频硬件抽象层类

在这里插入图片描述
audio_hal.h


#pragma once

#include <vector>
#include <cstdint>
class AudioHAL
{
private: 

public:
    AudioHAL();
    ~AudioHAL(); 
    virtual void enable_input(); 
    virtual void enable_output();
    virtual void disable_input();
    virtual void disable_output();
 
    virtual void write(std::vector<int16_t>& data);
    virtual int read(std::vector<int16_t>& data,int samples);
};

audio_hal.cpp

#include "audio_hal.h"

AudioHAL::AudioHAL()
{

}

AudioHAL::~AudioHAL()
{

}

这段代码定义了一个音频硬件抽象层(HAL)的基类 AudioHAL,本质是对「音频输入 / 输出」的通用操作(启用 / 禁用、读写音频数据)做了标准化抽象,目的是屏蔽不同音频硬件(比如 ES7210、ES8311)的底层差异,让上层代码能统一调用音频功能。

1.本质是音频硬件的通用抽象层基类,定义标准化接口;
2.屏蔽底层硬件差异,让上层代码统一调用音频输入 / 输出功能;
3.所有方法为虚函数,需子类实现具体硬件的操作逻辑。

2、创建硬件层文件

在这里插入图片描述
audio_es8311_es7210.h

#pragma once
#include "audio_hal.h"
#include <gpio_num.h>
class AudioEs8311Es7210: public AudioHAL
{
private:
    
public: 
    virtual void enable_input()override; 
    virtual void enable_output()override;
    virtual void disable_input()override;
    virtual void disable_output()override;

    virtual void write(std::vector<int16_t>& data)override;
    virtual int read(std::vector<int16_t>& data,int samples)override;

    AudioEs8311Es7210(void* i2c_bus_handle, int input_sample_rate, int output_sample_rate,
        gpio_num_t mclk_pin, gpio_num_t bclk_pin, gpio_num_t ws_pin, gpio_num_t dout_pin, gpio_num_t din_pin,
        gpio_num_t pa_pin,bool pa_reverted, uint8_t es8311_i2c_addr, uint8_t es7210_i2c_addr, bool input_ref);
    ~AudioEs8311Es7210();
};

audio_es8311_es7210.cpp

#include "audio_es8311_es7210.h"

AudioEs8311Es7210::AudioEs8311Es7210(void* i2c_bus_handle, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk_pin, gpio_num_t bclk_pin, gpio_num_t ws_pin, gpio_num_t dout_pin, gpio_num_t din_pin,
    gpio_num_t pa_pin,bool pa_reverted, uint8_t es8311_i2c_addr, uint8_t es7210_i2c_addr, bool input_ref)
{
    
}
AudioEs8311Es7210::~AudioEs8311Es7210()
{
}
 
void AudioEs8311Es7210::enable_input()
{
}

void AudioEs8311Es7210::enable_output()
{
}

void AudioEs8311Es7210::disable_input()
{

}

void AudioEs8311Es7210::disable_output()
{

}

int AudioEs8311Es7210::read(std::vector<int16_t>& data,int samples)
{
    return 0;
}

void AudioEs8311Es7210::write(std::vector<int16_t>& data)
{
}

定义了适配 ES8311(输出)+ ES7210(输入)音频芯片的 AudioEs8311Es7210 子类(继承通用音频抽象层 AudioHAL),仅声明并空实现了所有音频操作接口—— 包含芯片初始化的构造函数(接收引脚、I2C、采样率等硬件参数),以及输入输出启用 / 禁用、音频数据读写的方法,但暂未实现具体的芯片控制和数据传输逻辑。

1.继承通用音频抽象层,绑定 ES8311/ES7210 硬件配置参数;
2.覆写所有音频操作接口,但仅为空实现(无实际功能);
3.是后续填充 ES8311/ES7210 芯片具体驱动逻辑的 “骨架代码”。

三、代码具体实现(一)

1、安装依赖

在这里插入图片描述

idf.py add-dependency "espressif/esp_codec_dev^1.3.4"
idf.py reconfigure

在这里插入图片描述

2、创建类成员变量

在这里插入图片描述


#include <driver/i2s_types.h>
#include <audio_codec_data_if.h>
#include <audio_codec_ctrl_if.h>
#include <audio_codec_if.h>
#include <audio_codec_gpio_if.h>
#include <esp_codec_dev.h>




protected:
    i2s_chan_handle_t tx_handle = NULL;
    i2s_chan_handle_t rx_handle = NULL;
    int output_sample_rate=0;
    int input_sample_rate=0;
    const audio_codec_data_if_t* data_if = nullptr;
    const audio_codec_ctrl_if_t* out_ctrl_if = nullptr;
    const audio_codec_ctrl_if_t* in_ctrl_if = nullptr;
    const audio_codec_if_t* out_codec_if = nullptr;
    const audio_codec_if_t* in_codec_if = nullptr;  
    const audio_codec_gpio_if_t* gpio_if = nullptr;
    esp_codec_dev_handle_t audio_output_dev = nullptr;
    esp_codec_dev_handle_t audio_input_dev = nullptr;

新添加的变量是 ESP32 音频编解码的核心资源句柄 / 参数,涵盖 I2S 通道、编解码器接口、设备句柄和采样率。统一封装在基类中,实现音频资源的标准化管理,降低子类开发成本。

PS:
这些成员变量是音频框架的公共资源(比如 I2S 句柄、编解码器接口):
若把它们写在每个硬件子类(如AudioEs8311Es7210)里,会导致每个子类都重复定义相同变量,代码冗余;
用protected把它们放在基类AudioHAL中,既能让子类直接访问(避免冗余),又能阻止外部类随意修改(保证封装性)。
在这里插入图片描述

3、声明私有成员函数

在这里插入图片描述


#include <hal/i2c_types.h>


/**
 * @brief 初始化i2s通道
 * @param mclk_pin mclk引脚
 * @param bclk_pin bclk引脚
 * @param ws_pin ws引脚
 * @param dout_pin dout引脚
 * @param din_pin din引脚
 */
void init_i2s_channel(gpio_num_t mclk_pin,gpio_num_t bclk_pin,gpio_num_t ws_pin, gpio_num_t dout_pin,gpio_num_t din_pin);
// 初始化es8311
/**
 * @brief 初始化es8311
 * @param i2c_bus_handle i2c总线句柄
 * @param i2c_port i2c总线端口
 * @param es8311_i2c_addr es8311 i2c地址
 * @param pa_pin 功放使能引脚
 * @param pa_reverted 
 * pa_reverted = false表示非反转逻辑:当 pa_pin 引脚输出 1(高电平) 时,功放(PA)被使能;输出 0(低电平) 时,功放被禁用。
 * pa_reverted = true 表示反转逻辑:当 pa_pin 引脚输出 0(低电平) 时,功放被使能;输出 1(高电平) 时,功放被禁用。
 */
void init_es8311(void* i2c_bus_handle,i2c_port_t i2c_port,uint8_t es8311_i2c_addr,gpio_num_t pa_pin,bool pa_reverted);
/**
 * @brief 初始化es7210
 * @param i2c_bus_handle i2c总线句柄
 * @param i2c_port i2c总线端口
 * @param es7210_i2c_addr es7210 i2c地址
 */
void init_es7210(void* i2c_bus_handle,i2c_port_t i2c_port,uint8_t es7210_i2c_addr);

这里的三个函数对应的就是之前框架图里的三部分
在这里插入图片描述
功能写好了,具体还没调用,data的部分后面会写,写好了调用就是完整的流程

4、实现私有成员函数

在这里插入图片描述


#include <driver/i2s_common.h>
#include <driver/i2s_std.h>
#include <driver/i2s_tdm.h>
#include <esp_codec_dev_defaults.h>


void AudioEs8311Es7210::init_i2s_channel(gpio_num_t mclk_pin, gpio_num_t bclk_pin, gpio_num_t ws_pin, gpio_num_t dout_pin, gpio_num_t din_pin)
{
    i2s_chan_config_t chan_cfg = {
        .id = I2S_NUM_0,
        .role = I2S_ROLE_MASTER, //i2s的主从模式
        .auto_clear_after_cb = true,
        .auto_clear_before_cb = false,
        .intr_priority = 0,//中断优先级,数值越小优先级越高
    };
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));

    i2s_std_config_t std_cfg = {
        .clk_cfg = {
            .sample_rate_hz = (uint32_t)output_sample_rate,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .ext_clk_freq_hz = 0,
            .mclk_multiple = I2S_MCLK_MULTIPLE_256
        },
        .slot_cfg = {
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = I2S_STD_SLOT_BOTH,
            .ws_width = I2S_DATA_BIT_WIDTH_16BIT,
            .ws_pol = false,
            .bit_shift = true,
            .left_align = true,
            .big_endian = false,
            .bit_order_lsb = false
        },
        .gpio_cfg = {
            .mclk = mclk_pin,
            .bclk = bclk_pin,
            .ws = ws_pin,
            .dout = dout_pin,
            .din = GPIO_NUM_NC,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    i2s_tdm_config_t tdm_cfg = {
        .clk_cfg = {
            .sample_rate_hz = (uint32_t)input_sample_rate,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .ext_clk_freq_hz = 0,
            .mclk_multiple = I2S_MCLK_MULTIPLE_256,
            .bclk_div = 8,
        },
        .slot_cfg = {
            .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width = I2S_SLOT_BIT_WIDTH_AUTO,
            .slot_mode = I2S_SLOT_MODE_STEREO,
            .slot_mask = i2s_tdm_slot_mask_t(I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2),
            .ws_width = I2S_TDM_AUTO_WS_WIDTH,
            .ws_pol = false,
            .bit_shift = true,
            .left_align = false,
            .big_endian = false,
            .bit_order_lsb = false,
            .skip_mask = false,
            .total_slot = I2S_TDM_AUTO_SLOT_NUM
        },
        .gpio_cfg = {
            .mclk = mclk_pin,
            .bclk = bclk_pin,
            .ws = ws_pin,
            .dout = GPIO_NUM_NC,
            .din = din_pin,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv = false
            }
        }
    };

    ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
    ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle, &tdm_cfg));
}
void AudioEs8311Es7210::init_es8311(void* i2c_bus_handle,i2c_port_t i2c_port,uint8_t es8311_i2c_addr,gpio_num_t pa_pin,bool pa_reverted)
{ 
    // Output
    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = i2c_port,
        .addr = es8311_i2c_addr,
        .bus_handle = i2c_bus_handle,
    };
    out_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg); 

    gpio_if = audio_codec_new_gpio(); 
    es8311_codec_cfg_t es8311_cfg = {};
    es8311_cfg.ctrl_if = out_ctrl_if;
    es8311_cfg.gpio_if = gpio_if;
    es8311_cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC;
    es8311_cfg.pa_pin = pa_pin;
    es8311_cfg.pa_reverted = pa_reverted;
    es8311_cfg.use_mclk = true;
    es8311_cfg.hw_gain.pa_voltage = 5.0;
    es8311_cfg.hw_gain.codec_dac_voltage = 3.3;
    out_codec_if = es8311_codec_new(&es8311_cfg); 

    esp_codec_dev_cfg_t dev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
        .codec_if = out_codec_if,
        .data_if = data_if,
    };
    audio_output_dev = esp_codec_dev_new(&dev_cfg); 
}
void AudioEs8311Es7210::init_es7210(void* i2c_bus_handle,i2c_port_t i2c_port,uint8_t es7210_i2c_addr)
{ 

    audio_codec_i2c_cfg_t i2c_cfg = {
        .port = i2c_port,
        .addr = es7210_i2c_addr,
        .bus_handle = i2c_bus_handle,
    };
    in_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg); 

 

    es7210_codec_cfg_t es7210_cfg = {};
    es7210_cfg.ctrl_if = in_ctrl_if;
    es7210_cfg.mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2 | ES7120_SEL_MIC3 | ES7120_SEL_MIC4;
    in_codec_if = es7210_codec_new(&es7210_cfg);
    
    esp_codec_dev_cfg_t dev_cfg = {
        .dev_type = ESP_CODEC_DEV_TYPE_IN,
        .codec_if = in_codec_if,
        .data_if = data_if,
    };
  
    audio_input_dev = esp_codec_dev_new(&dev_cfg); 
}
  1. init_i2s_channel:初始化 I2S 总线通道
    先创建 I2S 主通道(生成 tx/rx 句柄);
    分别配置输出(std 模式)、输入(tdm 模式)的 I2S 参数(采样率、引脚、数据格式等);
    初始化 tx 通道为 std 模式(适配 ES8311 输出)、rx 通道为 tdm 模式(适配 ES7210 输入),完成音频数据传输链路搭建。
  2. init_es8311:初始化 ES8311 输出芯片
    配置 I2C 控制参数,创建输出控制接口;
    初始化 GPIO 接口,配置 ES8311 专属参数(功放引脚、工作模式为 DAC 等);
    创建 ES8311 核心接口,封装成标准音频输出设备句柄(audio_output_dev)。
  3. init_es7210:初始化 ES7210 输入芯片
    配置 I2C 控制参数,创建输入控制接口;
    配置 ES7210 专属参数(启用 4 路麦克风),创建 ES7210 核心接口;
    封装成标准音频输入设备句柄(audio_input_dev)。

5、编写构造函数

在这里插入图片描述


```csharp
AudioEs8311Es7210::AudioEs8311Es7210(void* i2c_bus_handle,i2c_port_t i2c_port, int input_sample_rate, int output_sample_rate,
    gpio_num_t mclk_pin, gpio_num_t bclk_pin, gpio_num_t ws_pin, gpio_num_t dout_pin, gpio_num_t din_pin,
    gpio_num_t pa_pin,bool pa_reverted, uint8_t es8311_i2c_addr, uint8_t es7210_i2c_addr, bool input_ref)
{
    this->input_sample_rate = input_sample_rate;
    this->output_sample_rate = output_sample_rate;
    
    init_i2s_channel(mclk_pin, bclk_pin, ws_pin, dout_pin, din_pin);


    audio_codec_i2s_cfg_t i2s_cfg = {
        .port = I2S_NUM_0,
        .rx_handle = rx_handle,
        .tx_handle = tx_handle,
    };
    data_if = audio_codec_new_i2s_data(&i2s_cfg); 


    init_es8311(i2c_bus_handle, i2c_port, es8311_i2c_addr, pa_pin, pa_reverted);
    init_es7210(i2c_bus_handle, i2c_port, es7210_i2c_addr);
    
}

其中这个部分对应
在这里插入图片描述

在这里插入图片描述

四、代码具体实现(二)

1、定义成员变量

在这里插入图片描述

bool enabled_input = false;
bool enabled_output = false;
bool input_ref=false;

enabled_input/enabled_output:记录音频输入 / 输出功能的 “启用状态”(false表示默认未启用),后续调用enable_input()/enable_output()时会把这些变量设为true,方便判断当前功能是否处于激活状态;
input_ref:记录音频输入的 “参考模式” 配置(构造函数传入的参数),用于后续 ES7210 芯片的硬件参数初始化。

2、获取输入参考参数

在这里插入图片描述
它的核心作用是告诉 ES7210 芯片:音频输入信号的参考基准是什么

3、实现代码

AudioEs8311Es7210::~AudioEs8311Es7210()
{
    esp_codec_dev_close(audio_input_dev);
    esp_codec_dev_close(audio_output_dev);
    esp_codec_dev_delete(audio_output_dev);
    esp_codec_dev_delete(audio_input_dev);
    audio_codec_delete_codec_if(in_codec_if);
    audio_codec_delete_ctrl_if(in_ctrl_if);
    audio_codec_delete_codec_if(out_codec_if);
    audio_codec_delete_ctrl_if(out_ctrl_if);
    audio_codec_delete_gpio_if(gpio_if);
    audio_codec_delete_data_if(data_if);
}
 
void AudioEs8311Es7210::enable_input()
{
    if (enabled_input) {
        return;
    } 
    esp_codec_dev_sample_info_t codec_dev = {
        .bits_per_sample = 16,
        .channel = 4,
        .channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
        .sample_rate = (uint32_t)input_sample_rate,
        .mclk_multiple = 0,
    };
    if (input_ref) {
        codec_dev.channel_mask |= ESP_CODEC_DEV_MAKE_CHANNEL_MASK(1);
    }
    ESP_ERROR_CHECK(esp_codec_dev_open(audio_input_dev, &codec_dev));
    ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(audio_input_dev, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 40.0));
    enabled_input= true;
}

void AudioEs8311Es7210::enable_output()
{
    if (enabled_output) {
        return;
    }  
    esp_codec_dev_sample_info_t codec_dev = {
        .bits_per_sample = 16,
        .channel = 1,
        .channel_mask = 0,
        .sample_rate = (uint32_t)output_sample_rate,
        .mclk_multiple = 0,
    };
    ESP_ERROR_CHECK(esp_codec_dev_open(audio_output_dev, &codec_dev)); 
    enabled_output = true;
}

void AudioEs8311Es7210::disable_input()
{
    ESP_ERROR_CHECK(esp_codec_dev_close(audio_input_dev));
    enabled_input = false;
}

void AudioEs8311Es7210::disable_output()
{
    ESP_ERROR_CHECK(esp_codec_dev_close(audio_output_dev));
    enabled_output = false;
}

int AudioEs8311Es7210::read(int16_t* data,int samples)
{
    if (enabled_input) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_read(audio_input_dev, (void*)data , samples * sizeof(int16_t)));
    }
    return samples * sizeof(int16_t);
}

void AudioEs8311Es7210::write(const int16_t* data)
{
    if (enabled_output) {
        ESP_ERROR_CHECK_WITHOUT_ABORT(esp_codec_dev_write(audio_output_dev, (void*)data, sizeof(data) * sizeof(int16_t)));
    }
}

AudioEs8311Es7210 核心函数功能说明

函数 核心操作
析构函数 ~AudioEs8311Es7210 释放所有音频资源:关闭/删除设备句柄、销毁编解码器接口(控制/数据/GPIO),避免内存泄漏
enable_input 启用录音功能:
1. 检查状态避免重复启用;
2. 配置 ES7210 采样参数(16 位、4 通道、采样率);
3. 根据input_ref调整通道掩码;
4. 打开输入设备并设置麦克风增益;
5. 标记输入为启用状态
enable_output 启用播放功能:
1. 检查状态避免重复启用;
2. 配置 ES8311 采样参数(16 位、单通道、采样率);
3. 打开输出设备;
4. 标记输出为启用状态
disable_input/disable_output 禁用录音 / 播放功能:关闭对应设备,重置启用状态
read 读取录音数据:仅当输入启用时,从音频输入设备读取指定数量的 16 位音频数据
write 写入播放数据:仅当输出启用时,向音频输出设备写入 16 位音频数据(注:sizeof(data) 是笔误,应为数据长度)

4、格式调整

适配之前的函数实现
在这里插入图片描述

    virtual void write(const int16_t* data)override;
    virtual int read(int16_t* data,int samples)override;

在这里插入图片描述

    virtual void write(const int16_t* data);
    virtual int read(int16_t* dest,int samples);
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐