【AI小智硬件程序(四)】

AI小智硬件程序(四)

链接: B站Up

SP32S3实现麦克风录音保存到SD卡

一、核心硬件与原理

1.核心芯片

ES7210:乐鑫官方推荐的 ADC 芯片,支持 I²C 配置寄存器I²S 接口读取音频数据 ,最多支持 4 路麦克风,本项目使用 2 路(含 1 路回声消除)。

模拟麦克风:输出模拟音频信号,由 ES7210 完成模数转换。

ESP32:通过 I²C 配置 ES7210 参数,通过 I²S 接收音频数据,再将数据写入 SD 卡。

2.通信接口原理

I²C:用于配置 ES7210 的寄存器(如麦克风供电电压、增益、采样频率),设备地址为 0X41。

I²C 配置:本质是 "给 ES7210 芯片发配置命令"------ 通过 I²C 总线往 ES7210 的寄存器里写数值,比如告诉它 "给麦克风供 3.3V 电""采样频率设为 16KHz""增益调 20dB"。
I²S:用于传输 ADC 转换后的数字音频数据,采用 TDM 模式,支持立体声(双声道)采集。

I²S 配置:本质是 "约定 ESP32 和 ES7210 传输音频数据的规则"------ 比如 "数据按 16bit 传输""双声道交替传""时钟频率多少",只有规则一致,ESP32 才能正确解析 ES7210 传过来的音频数据。

二、开发流程步骤

步骤 1:添加 ES7210 官方组件依赖

组件获取:通过 ESP-IDF 的依赖管理工具,搜索并安装 ES7210 组件,命令会自动将组件添加到项目的 yml 依赖文件中。

组件拉取:运行指令拉取组件,项目自动生成 第三方组件文件夹,内含 ES7210 驱动代码,无需手动编写底层驱动。

bash 复制代码
idf.py add-dependency "espressif/es7210^1.0.1~1"

//执行完上面的命令执行这个命令
idf.py reconfigure


步骤 2:创建核心代码文件

在项目驱动目录下新建以下文件,用于封装驱动逻辑和 WAV 格式处理:

  • 添加format_wav.h
cpp 复制代码
/*
 * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */
#pragma once

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Header structure for WAV file with only one data chunk
 *
 * @note See this for reference: http://soundfile.sapp.org/doc/WaveFormat/
 *
 * @note Assignment to variables in this struct directly is only possible for little endian architectures
 *       (including Xtensa & RISC-V)
 */
typedef struct {
    struct {
        char chunk_id[4]; /*!< Contains the letters "RIFF" in ASCII form */
        uint32_t chunk_size; /*!< This is the size of the rest of the chunk following this number */
        char chunk_format[4]; /*!< Contains the letters "WAVE" */
    } descriptor_chunk; /*!< Canonical WAVE format starts with the RIFF header */
    struct {
        char subchunk_id[4]; /*!< Contains the letters "fmt " */
        uint32_t subchunk_size; /*!< This is the size of the rest of the Subchunk which follows this number */
        uint16_t audio_format; /*!< PCM = 1, values other than 1 indicate some form of compression */
        uint16_t num_of_channels; /*!< Mono = 1, Stereo = 2, etc. */
        uint32_t sample_rate; /*!< 8000, 44100, etc. */
        uint32_t byte_rate; /*!< ==SampleRate * NumChannels * BitsPerSample s/ 8 */
        uint16_t block_align; /*!< ==NumChannels * BitsPerSample / 8 */
        uint16_t bits_per_sample; /*!< 8 bits = 8, 16 bits = 16, etc. */
    } fmt_chunk; /*!< The "fmt " subchunk describes the sound data's format */
    struct {
        char subchunk_id[4]; /*!< Contains the letters "data" */
        uint32_t subchunk_size; /*!< ==NumSamples * NumChannels * BitsPerSample / 8 */
        int16_t data[0]; /*!< Holds raw audio data */
    } data_chunk; /*!< The "data" subchunk contains the size of the data and the actual sound */
} wav_header_t;

/**
 * @brief Default header for PCM format WAV files
 *
 */
#define WAV_HEADER_PCM_DEFAULT(wav_sample_size, wav_sample_bits, wav_sample_rate, wav_channel_num) { \
    .descriptor_chunk = { \
        .chunk_id = {'R', 'I', 'F', 'F'}, \
        .chunk_size = (wav_sample_size) + sizeof(wav_header_t) - 8, \
        .chunk_format = {'W', 'A', 'V', 'E'} \
    }, \
    .fmt_chunk = { \
        .subchunk_id = {'f', 'm', 't', ' '}, \
        .subchunk_size = 16, /* 16 for PCM */ \
        .audio_format = 1, /* 1 for PCM */ \
        .num_of_channels = (wav_channel_num), \
        .sample_rate = (wav_sample_rate), \
        .byte_rate = (wav_sample_bits) * (wav_sample_rate) * (wav_channel_num) / 8, \
        .block_align = (wav_sample_bits) * (wav_channel_num) / 8, \
        .bits_per_sample = (wav_sample_bits)\
    }, \
    .data_chunk = { \
        .subchunk_id = {'d', 'a', 't', 'a'}, \
        .subchunk_size = (wav_sample_size) \
    } \
}

#ifdef __cplusplus
}
#endif

这段代码是 ESP-IDF 官方提供的 WAV 音频文件头部(Header)定义,核心作用是标准化 WAV 文件的格式结构,让你能通过代码快速构建合法的 WAV 文件头部,从而把麦克风采集的 PCM 原始音频数据封装成可播放的 WAV 文件。

  • 修改 CMakeLists.txt:将新增的 .cpp 文件和头文件路径添加到编译配置中,确保编译器能识别代码。
cpp 复制代码
# idf_component_register(SRCS "app.cpp"
#                     INCLUDE_DIRS ".")


set(SOURCES "app.cpp"
    "drivers/storage/sd_card.cpp" 
    "drivers/audio/audio_es7210.cpp" 
    "drivers/audio/wav_recorder.cpp" 
)

set(INCLUDE_DIRS "." 
            "drivers"
            "drivers/storage"
            "drivers/audio/"
)


idf_component_register(SRCS ${SOURCES} 
                INCLUDE_DIRS ${INCLUDE_DIRS} 
                )

步骤 3:ES7210 芯片初始化配置

  • audio_es7210.h/audio_es7210.cpp:ES7210 芯片的初始化、I²C/I²S 配置代码。
cpp 复制代码
#pragma once
#include <driver/i2s_types.h>
#include <driver/i2s_common.h>
#include <driver/i2s.h>
#include <driver/i2s_tdm.h>



/* I2C port and GPIOs */
#define EXAMPLE_I2C_NUM            (I2C_NUM_0)
#define EXAMPLE_I2C_SDA_IO         GPIO_NUM_2
#define EXAMPLE_I2C_SCL_IO         GPIO_NUM_1

/* I2S port and GPIOs */
#define EXAMPLE_I2S_NUM            (I2S_NUM_0)
#define EXAMPLE_I2S_MCK_IO         GPIO_NUM_38
#define EXAMPLE_I2S_BCK_IO         GPIO_NUM_14
#define EXAMPLE_I2S_WS_IO          GPIO_NUM_13
#define EXAMPLE_I2S_DI_IO          GPIO_NUM_12
 
/* I2S configurations */
#define EXAMPLE_I2S_TDM_FORMAT     (ES7210_I2S_FMT_I2S)
#define EXAMPLE_I2S_CHAN_NUM       (2)
#define EXAMPLE_I2S_SAMPLE_RATE    (16000)
#define EXAMPLE_I2S_MCLK_MULTIPLE  (I2S_MCLK_MULTIPLE_256)
#define EXAMPLE_I2S_SAMPLE_BITS    (I2S_DATA_BIT_WIDTH_16BIT)
#define EXAMPLE_I2S_TDM_SLOT_MASK ((i2s_tdm_slot_mask_t)(I2S_TDM_SLOT0 | I2S_TDM_SLOT1))
 
#define EXAMPLE_ES7210_I2C_ADDR    (0x41)
#define EXAMPLE_ES7210_I2C_CLK      (100000)
#define EXAMPLE_ES7210_MIC_BIAS     (ES7210_MIC_BIAS_2V87)
#define EXAMPLE_ES7210_MIC_GAIN     (ES7210_MIC_GAIN_30DB)   
#define EXAMPLE_ES7210_ADC_VOLUME     (0)


#define EXAMPLE_RECORD_TIME_SEC (10)

class AudioEs7210
{
private:
    i2s_chan_handle_t es7210_i2s_init(void);
    void es7210_code_init(void);
    /* data */
public: 
    i2s_chan_handle_t audio_es7210_init(void); 
}; 

基于 ESP32 的 ES7210 音频采集芯片的配置与封装头文件,核心是通过宏定义固化硬件 / 功能参数,并以 C++ 类的形式封装 ES7210 初始化逻辑,最终对外提供统一的初始化接口,实现 ES7210 麦克风采集的标准化配置与调用。

private 部分:隐藏 I2S 通道初始化、ES7210 芯片寄存器配置的内部子步骤,避免外部错误调用;

public 部分:暴露唯一的初始化入口函数 audio_es7210_init,外部只需调用该函数,即可按预设参数完成 I2S 通道 + ES7210 芯片的完整初始化,并返回 I2S 接收通道句柄(供后续读取音频数据)

c 复制代码
#include "audio_es7210.h" 
#include "esp_log.h"
#include "esp_err.h" 
#include "esp_log.h"
#include "esp_err.h" 
#include <cstdio>
#include <cstring>
#include <driver/i2s_tdm.h> 
#include <driver/i2c.h>
#include <es7210.h>
#include <esp_check.h>


#define TAG "ES7210"
i2s_chan_handle_t AudioEs7210::es7210_i2s_init(void)
{
    i2s_chan_handle_t i2s_rx_chan = NULL;  // 定义接收通道句柄 
    i2s_chan_config_t i2s_rx_conf = I2S_CHANNEL_DEFAULT_CONFIG(EXAMPLE_I2S_NUM, I2S_ROLE_MASTER); // 配置接收通道
    ESP_ERROR_CHECK(i2s_new_channel(&i2s_rx_conf, NULL, &i2s_rx_chan)); // 创建i2s通道 
    ESP_LOGI(TAG, "Configure I2S receive channel to TDM mode"); 

    // 定义接收通道为I2S TDM模式 并配置
    i2s_tdm_config_t i2s_tdm_rx_conf = {
        .clk_cfg  = {
            .sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .mclk_multiple = EXAMPLE_I2S_MCLK_MULTIPLE
        },
        .slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(EXAMPLE_I2S_SAMPLE_BITS, I2S_SLOT_MODE_STEREO, EXAMPLE_I2S_TDM_SLOT_MASK),
        
        .gpio_cfg = {
            .mclk = EXAMPLE_I2S_MCK_IO,
            .bclk = EXAMPLE_I2S_BCK_IO,
            .ws   = EXAMPLE_I2S_WS_IO,
            .dout = GPIO_NUM_NC, // ES7210 only has ADC capability
            .din  = EXAMPLE_I2S_DI_IO
        },
    };

    ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(i2s_rx_chan, &i2s_tdm_rx_conf)); // 初始化I2S通道为TDM模式

    return i2s_rx_chan;
}


/**
 * @brief 初始化ES7210芯片
 */
void AudioEs7210::es7210_code_init(void)
{
    // 初始化I2C接口
    ESP_LOGI(TAG, "Init I2C used to configure ES7210");
    i2c_config_t i2c_conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = EXAMPLE_I2C_SDA_IO,
        .scl_io_num = EXAMPLE_I2C_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master{
            .clk_speed = EXAMPLE_ES7210_I2C_CLK,
        }
    };
    ESP_ERROR_CHECK(i2c_param_config(EXAMPLE_I2C_NUM, &i2c_conf));
    ESP_ERROR_CHECK(i2c_driver_install(EXAMPLE_I2C_NUM, i2c_conf.mode, 0, 0, 0));

    // 配置es7210器件句柄
    es7210_dev_handle_t es7210_handle = NULL;
    es7210_i2c_config_t es7210_i2c_conf = {
        .i2c_port = EXAMPLE_I2C_NUM,
        .i2c_addr = EXAMPLE_ES7210_I2C_ADDR
    };
    ESP_ERROR_CHECK(es7210_new_codec(&es7210_i2c_conf, &es7210_handle));

    // 初始化es7210芯片
    ESP_LOGI(TAG, "Configure ES7210 codec parameters");
    es7210_codec_config_t codec_conf = {
        .sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
        .mclk_ratio = EXAMPLE_I2S_MCLK_MULTIPLE,
        .i2s_format = EXAMPLE_I2S_TDM_FORMAT,
        .bit_width = (es7210_i2s_bits_t)EXAMPLE_I2S_SAMPLE_BITS,
        .mic_bias = EXAMPLE_ES7210_MIC_BIAS,
        .mic_gain = EXAMPLE_ES7210_MIC_GAIN,
        .flags{
            .tdm_enable = true
        }
    };
    ESP_ERROR_CHECK(es7210_config_codec(es7210_handle, &codec_conf));
    ESP_ERROR_CHECK(es7210_config_volume(es7210_handle, EXAMPLE_ES7210_ADC_VOLUME));
}


i2s_chan_handle_t AudioEs7210::audio_es7210_init(void)
{
    ESP_LOGI(TAG, "Initialize ES7210 codec");
    i2s_chan_handle_t i2s_rx_chan = es7210_i2s_init();
    es7210_code_init();
    return i2s_rx_chan;
}

 

这段代码是 ES7210 音频采集芯片的初始化实现,核心是完成 ESP32 与 ES7210 之间的 I2S(音频数据传输)和 I2C(芯片配置)接口初始化,最终返回可用于读取音频数据的 I2S 接收通道句柄。

步骤 4:WAV 格式音频封装与存储

  • wav_recoder.h/wav_recoder.cpp:WAV 音频文件的头部封装、数据写入逻辑。
cpp 复制代码
#pragma once
#include "file_interface.h"
#include "driver/i2s_std.h"
#include <memory>  

class WavRecorder {
public:
    explicit WavRecorder(std::shared_ptr<FileInterface> fileInterface);

    esp_err_t record(i2s_chan_handle_t i2s_chan, uint16_t seconds);
    
private:
    std::shared_ptr<FileInterface> m_file; // 用智能指针持有对象
};

定义 WAV 录音类接口,通过智能指针注入文件操作接口,封装录音逻辑对外暴露

cpp 复制代码
#include "wav_recorder.h"
#include "format_wav.h"
#include "audio_es7210.h"
#include <esp_timer.h>
#include <esp_check.h>

#define TAG "WavRecorder"

WavRecorder::WavRecorder(std::shared_ptr<FileInterface> fileInterface) 
: m_file(fileInterface) {

} 

esp_err_t WavRecorder::record(i2s_chan_handle_t i2s_chan, uint16_t seconds) {

    esp_err_t ret = ESP_OK; 
    // 计算音频数据大小
    uint32_t byte_rate = EXAMPLE_I2S_SAMPLE_RATE * EXAMPLE_I2S_CHAN_NUM * EXAMPLE_I2S_SAMPLE_BITS / 8;
    uint32_t wav_size = seconds * byte_rate;
    
    // 创建初始WAV头部(数据大小暂时设为0)
    wav_header_t wav_header = WAV_HEADER_PCM_DEFAULT(wav_size, EXAMPLE_I2S_SAMPLE_BITS, 
                                                EXAMPLE_I2S_SAMPLE_RATE, 
                                                EXAMPLE_I2S_CHAN_NUM);
 
    // 1. 写入WAV头部
    if (m_file->write_file((char *)&wav_header,sizeof(wav_header))!=ESP_OK) {
        ESP_LOGE(TAG, "写入WAV头部失败");
        return ESP_FAIL;
    }
    
    // 2. 实时录音并分段写入数据
    /* Start recording */
    size_t wav_written = 0;
    static int16_t i2s_readraw_buff[8192];
     

    //使能通道,如果出错直接跳到err标签位置
    ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_chan), err, TAG, "error while starting i2s rx channel");

    while (wav_written < wav_size) {
        if(wav_written % byte_rate < sizeof(i2s_readraw_buff)) {
            ESP_LOGI(TAG, "Recording: %"PRIu32"/%ds", wav_written/byte_rate + 1, EXAMPLE_RECORD_TIME_SEC);
            printf(".");
        }
        size_t bytes_read = 0;
        /* Read RAW samples from ES7210 */
        ESP_GOTO_ON_ERROR(i2s_channel_read(i2s_chan, i2s_readraw_buff, sizeof(i2s_readraw_buff), &bytes_read,
                          pdMS_TO_TICKS(1000)), err, TAG, "error while reading samples from i2s");
        /* Write the samples to the WAV file */
        // ESP_GOTO_ON_FALSE(m_file->write_file(filename,(char *)i2s_readraw_buff,true), ESP_FAIL, err,
        // TAG, "error while writing samples to wav file");


        if (m_file->write_file( (char *)i2s_readraw_buff,bytes_read)!=ESP_OK) {
            ESP_LOGE(TAG, "写入音频数据失败");
            goto err;
        }

        wav_written += bytes_read;
    } 

    printf("录制完成\n");
err:
    i2s_channel_disable(i2s_chan);
    return ret;

}
 
 

实现录音核心逻辑:生成 WAV 头部、读取 I2S 音频数据、写入文件,含完善的错误处理

① 计算录音总数据量 → ② 生成 WAV 头部并写入文件 → ③ 启用 I2S 通道 → ④ 循环读取 ES7210 的 PCM 数据 → ⑤ 写入文件 → ⑥ 完成 / 出错后禁用 I2S 通道;

数据声音传输

模拟声音 → 麦克风 → ES7210(ADC 转 PCM)→ I2S 总线 → ESP32 I2S 通道 → 内存缓冲区 → WAV 封装(头部+数据)→ FileInterface → SD 卡(WAV 文件)

步骤 5:代码测试与验证

c 复制代码
// main.cpp
#include "WavRecorder.hpp"   // 引入WAV录音类(负责将I2S数据封装为WAV文件)
#include "SdCardFile.hpp"    // 引入SD卡文件操作类(实现FileInterface接口,负责文件读写)

// ESP32的主入口函数(程序从这里开始执行)
extern "C" void app_main() {
    // 1. 创建SD卡文件操作对象(智能指针自动管理内存,避免泄漏)
    //    std::make_shared<SdCard>():实例化SD卡操作类,后续用于WAV文件的读写
    auto sdFile = std::make_shared<SdCard>();
    
    // 2. 打开SD卡中的"AAA.wav"文件,模式为"w+"(可读可写,不存在则创建,存在则覆盖)
    //    ESP_ERROR_CHECK:如果打开失败,直接终止程序并打印错误
    ESP_ERROR_CHECK(sdFile->open("AAA.wav","w+"));
    
    // 3. 创建WavRecorder录音对象,通过构造函数注入SD卡文件操作对象(依赖倒置)
    //    这样WavRecorder无需关心存储介质(SD卡/闪存),只需调用FileInterface接口即可写文件
    WavRecorder recorder(sdFile);
    
    // 4. 创建ES7210音频采集对象,用于初始化ES7210芯片和I2S通道
    AudioEs7210 audioEs7210 = AudioEs7210();
    
    // 5. 初始化ES7210芯片和I2S接收通道
    //    返回I2S通道句柄(i2sChan),后续通过该句柄读取ES7210采集的音频数据
    i2s_chan_handle_t i2sChan = audioEs7210.audio_es7210_init();
    
    // 6. 开始录音:传入I2S通道句柄,指定录音时长10秒
    //    内部逻辑:生成WAV头部→启用I2S通道→读取PCM数据→写入SD卡→完成后禁用I2S通道
    recorder.record(i2sChan, 10);

    // 7. 录音完成后,关闭SD卡文件(释放文件资源,保证数据写入完成)
    sdFile->close();
}

在 ESP32 上完成 10 秒音频的采集与存储 ------ 通过 ES7210 芯片采集麦克风的声音,转换成标准 WAV 格式的数字音频文件(命名为 AAA.wav),并将文件保存到 SD 卡中。

麦克风录音功能代码重构

1. 问题痛点(重构前)

上层调用录音功能时,需要直接操作 I2S 句柄、关心 i2s_channel_enable/disable 等硬件细节;录音类(WavRecorder)直接依赖 I2S 底层实现,耦合度高,换其他音频采集方式(如其他芯片)需大幅修改代码。

2.核心解决方案:定义抽象接口类

创建音频输入抽象接口,定义通用方法(与底层无关):

enable():使能音频采集(无需传参,底层自行处理);

disable():禁用音频采集(无需传参);

read(char* buff, size_t buff_size, size_t& bytes_read):读取音频数据(仅传缓冲区、大小,返回实际读取字节数,不暴露句柄);

接口方法设为纯虚函数(=0),强制子类(如 ES7210 实现类)必须复写,保证接口统一。

cpp 复制代码
// file_interface.h
#pragma once
#include <cstddef>
#include <esp_err.h> 

 

class AudioInputInterface {
public:
    virtual ~AudioInputInterface() = default; // 基类的析构函数一定要是虚(virtual)函数,这样在销毁派生类的对象时才能正确调用派生类的析构函数
 
    virtual esp_err_t enable() = 0;
    virtual esp_err_t disable() = 0; 
    virtual esp_err_t read( void *dest, size_t size, size_t *bytes_read) = 0;
};

PS:

语法符号 / 关键字 核心作用(结合音频接口场景)

~ 析构函数的标志,对象销毁时自动调用,用于清理资源(如 ES7210 的 I2S 通道);

virtual 开启多态机制,保证子类重写的函数能被正确调用(如enable()),虚析构则保证子类析构函数执行;

= 0 标记纯虚函数,强制子类必须实现该函数(如read()),且接口类无法实例化;

3. 适配底层实现:ES7210 类继承并实现接口

AudioEs7210 类继承抽象接口 ,复写 enable/disable/read 方法:

  • enable():内部调用 i2s_channel_enable(m_i2s_chan)(句柄私有化,上层不可见);
  • disable():内部调用 i2s_channel_disable(m_i2s_chan);
  • read():内部调用 i2s_channel_read(m_i2s_chan, ...)(句柄、硬件细节全部封装在内部);

把 I2S 句柄 m_i2s_chan 设为 AudioEs7210 的私有成员,初始化逻辑移到构造函数中(支持传入采样率、声道数等参数,默认 16KHz / 双声道),不再返回句柄给上层。

audio_esp7210.h

cpp 复制代码
#pragma once
#include <driver/i2s_types.h>
#include <driver/i2s_common.h>
#include <driver/i2s.h>
#include <driver/i2s_tdm.h>
#include "audio_input_interface.h"



/* I2C port and GPIOs */
#define EXAMPLE_I2C_NUM            (I2C_NUM_0)
#define EXAMPLE_I2C_SDA_IO         GPIO_NUM_2
#define EXAMPLE_I2C_SCL_IO         GPIO_NUM_1

/* I2S port and GPIOs */
#define EXAMPLE_I2S_NUM            (I2S_NUM_0)
#define EXAMPLE_I2S_MCK_IO         GPIO_NUM_38
#define EXAMPLE_I2S_BCK_IO         GPIO_NUM_14
#define EXAMPLE_I2S_WS_IO          GPIO_NUM_13
#define EXAMPLE_I2S_DI_IO          GPIO_NUM_12
 
/* I2S configurations */
#define EXAMPLE_I2S_TDM_FORMAT     (ES7210_I2S_FMT_I2S)
#define EXAMPLE_I2S_CHAN_NUM       (2)
#define EXAMPLE_I2S_SAMPLE_RATE    (16000)
#define EXAMPLE_I2S_MCLK_MULTIPLE  (I2S_MCLK_MULTIPLE_256)
#define EXAMPLE_I2S_SAMPLE_BITS    (I2S_DATA_BIT_WIDTH_16BIT)
#define EXAMPLE_I2S_TDM_SLOT_MASK ((i2s_tdm_slot_mask_t)(I2S_TDM_SLOT0 | I2S_TDM_SLOT1))
 
#define EXAMPLE_ES7210_I2C_ADDR    (0x41)
#define EXAMPLE_ES7210_I2C_CLK      (100000)
#define EXAMPLE_ES7210_MIC_BIAS     (ES7210_MIC_BIAS_2V87)
#define EXAMPLE_ES7210_MIC_GAIN     (ES7210_MIC_GAIN_30DB)   
#define EXAMPLE_ES7210_ADC_VOLUME     (0)


#define EXAMPLE_RECORD_TIME_SEC (10)

class AudioEs7210: public AudioInputInterface
{
private:
    i2s_chan_handle_t es7210_i2s_init(void);
    void es7210_code_init(void);
    uint32_t sample_rate;
    uint8_t channel_num;
    i2s_chan_handle_t i2s_chan;
    /* data */
public: 

    esp_err_t disable() override;
    esp_err_t enable() override;
    esp_err_t read(void *dest, size_t size, size_t *bytes_read) override;
    AudioEs7210(uint32_t sample_rate=16000,uint8_t channel_num=2);

}; 

audio_esp7210.cpp

cpp 复制代码
#include "audio_es7210.h" 
#include "esp_log.h"
#include "esp_err.h" 
#include "esp_log.h"
#include "esp_err.h" 
#include <cstdio>
#include <cstring>
#include <driver/i2s_tdm.h> 
#include <driver/i2c.h>
#include <es7210.h>
#include <esp_check.h>  


#define TAG "ES7210"
esp_err_t AudioEs7210::enable()
{
    return i2s_channel_enable(i2s_chan);
}

esp_err_t AudioEs7210::disable()
{
    return i2s_channel_disable(i2s_chan);;
}
esp_err_t AudioEs7210::read(void *dest, size_t size, size_t *bytes_read)
{
    return i2s_channel_read(i2s_chan, dest, size, bytes_read,pdMS_TO_TICKS(1000));
}


AudioEs7210::AudioEs7210(uint32_t sample_rate,uint8_t channel_num):sample_rate(sample_rate),channel_num(channel_num){
    i2s_chan = es7210_i2s_init(); 
    es7210_code_init(); 
}
i2s_chan_handle_t AudioEs7210::es7210_i2s_init(void)
{
    i2s_chan_handle_t i2s_rx_chan = NULL;  // 定义接收通道句柄 
    i2s_chan_config_t i2s_rx_conf = I2S_CHANNEL_DEFAULT_CONFIG(EXAMPLE_I2S_NUM, I2S_ROLE_MASTER); // 配置接收通道
    ESP_ERROR_CHECK(i2s_new_channel(&i2s_rx_conf, NULL, &i2s_rx_chan)); // 创建i2s通道 
    ESP_LOGI(TAG, "Configure I2S receive channel to TDM mode"); 

    // 定义接收通道为I2S TDM模式 并配置
    i2s_tdm_config_t i2s_tdm_rx_conf = {
        .clk_cfg  = {
            .sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
            .clk_src = I2S_CLK_SRC_DEFAULT,
            .mclk_multiple = EXAMPLE_I2S_MCLK_MULTIPLE
        },
        .slot_cfg = I2S_TDM_PHILIPS_SLOT_DEFAULT_CONFIG(EXAMPLE_I2S_SAMPLE_BITS, I2S_SLOT_MODE_STEREO, EXAMPLE_I2S_TDM_SLOT_MASK),
        
        .gpio_cfg = {
            .mclk = EXAMPLE_I2S_MCK_IO,
            .bclk = EXAMPLE_I2S_BCK_IO,
            .ws   = EXAMPLE_I2S_WS_IO,
            .dout = GPIO_NUM_NC, // ES7210 only has ADC capability
            .din  = EXAMPLE_I2S_DI_IO
            
        },

    };

    ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(i2s_rx_chan, &i2s_tdm_rx_conf)); // 初始化I2S通道为TDM模式

    return i2s_rx_chan;
}


/**
 * @brief 初始化ES7210芯片
 */
void AudioEs7210::es7210_code_init(void)
{
    // 初始化I2C接口
    ESP_LOGI(TAG, "Init I2C used to configure ES7210");
    i2c_config_t i2c_conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = EXAMPLE_I2C_SDA_IO,
        .scl_io_num = EXAMPLE_I2C_SCL_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master{
            .clk_speed = EXAMPLE_ES7210_I2C_CLK,
        }
    };
    ESP_ERROR_CHECK(i2c_param_config(EXAMPLE_I2C_NUM, &i2c_conf));
    ESP_ERROR_CHECK(i2c_driver_install(EXAMPLE_I2C_NUM, i2c_conf.mode, 0, 0, 0));

    // 配置es7210器件句柄
    es7210_dev_handle_t es7210_handle = NULL;
    es7210_i2c_config_t es7210_i2c_conf = {
        .i2c_port = EXAMPLE_I2C_NUM,
        .i2c_addr = EXAMPLE_ES7210_I2C_ADDR
    };
    ESP_ERROR_CHECK(es7210_new_codec(&es7210_i2c_conf, &es7210_handle));

    // 初始化es7210芯片
    ESP_LOGI(TAG, "Configure ES7210 codec parameters");
    es7210_codec_config_t codec_conf = {
        .sample_rate_hz = EXAMPLE_I2S_SAMPLE_RATE,
        .mclk_ratio = EXAMPLE_I2S_MCLK_MULTIPLE,
        .i2s_format = EXAMPLE_I2S_TDM_FORMAT,
        .bit_width = (es7210_i2s_bits_t)EXAMPLE_I2S_SAMPLE_BITS,
        .mic_bias = EXAMPLE_ES7210_MIC_BIAS,
        .mic_gain = EXAMPLE_ES7210_MIC_GAIN,
        .flags{
            .tdm_enable = true
        }
    };
    ESP_ERROR_CHECK(es7210_config_codec(es7210_handle, &codec_conf));
    ESP_ERROR_CHECK(es7210_config_volume(es7210_handle, EXAMPLE_ES7210_ADC_VOLUME));
} 

重构前后核心差异对比

维度 重构前(原有代码) 重构后(当前代码)
类的定位 独立的 ES7210 操作类,对外暴露 I2S 句柄,上层需直接处理硬件细节 继承 AudioInputInterface 接口类,成为 "音频输入接口的 ES7210 实现",对外仅暴露接口方法
对外接口 暴露 audio_es7210_init(),返回 i2s_chan_handle_t 句柄,上层需传句柄调用 仅暴露 enable()/disable()/read()(接口方法),无句柄暴露,上层无需关心 I2S
初始化逻辑 手动调用初始化函数获取句柄,采样率 / 声道数写死 构造函数自动完成 I2S+ES7210 初始化,采样率 / 声道数支持传参(有默认值),更灵活
上层调用方式 需关心 I2S 句柄、手动使能 / 禁用通道、传句柄读数据 只需通过 AudioInputInterface 接口调用方法,无需知道底层是 ES7210/I2S
扩展性 换音频采集硬件需大幅修改上层代码 换硬件只需新增接口实现类,上层代码无需改动(符合依赖倒置)

4. 简化上层调用:录音类依赖抽象接口

修改 WavRecorder 类,构造函数接收 std::shared_ptr(抽象接口指针),而非直接依赖 AudioEs7210;

WavRecorder::record() 方法中,不再传 I2S 句柄,而是调用接口的enable()/read()/disable()

wave_recorder.h

cpp 复制代码
#pragma once
#include "file_interface.h"
#include "driver/i2s_std.h"
#include <memory>  
#include "audio_input_interface.h"

class WavRecorder {
public:
    explicit WavRecorder(std::shared_ptr<FileInterface> fileInterface,std::shared_ptr<AudioInputInterface> audioInput);

    esp_err_t record(uint16_t seconds);
    
private:
    std::shared_ptr<FileInterface> m_file; // 用智能指针持有对象
    std::shared_ptr<AudioInputInterface> audio_input; // 用智能指针持有对象
};

wave_recorder.cpp

cpp 复制代码
#include "wav_recorder.h"
#include "format_wav.h"
#include "audio_es7210.h"
#include <esp_timer.h>
#include <esp_check.h>

#define TAG "WavRecorder"

WavRecorder::WavRecorder(std::shared_ptr<FileInterface> fileInterface,std::shared_ptr<AudioInputInterface> audioInput) 
: m_file(fileInterface),audio_input(audioInput) {

} 

esp_err_t WavRecorder::record( uint16_t seconds) {

    esp_err_t ret = ESP_OK; 
    // 计算音频数据大小
    uint32_t byte_rate = EXAMPLE_I2S_SAMPLE_RATE * EXAMPLE_I2S_CHAN_NUM * EXAMPLE_I2S_SAMPLE_BITS / 8;
    uint32_t wav_size = seconds * byte_rate;
    
    // 创建初始WAV头部(数据大小暂时设为0)
    wav_header_t wav_header = WAV_HEADER_PCM_DEFAULT(wav_size, EXAMPLE_I2S_SAMPLE_BITS, 
                                                EXAMPLE_I2S_SAMPLE_RATE, 
                                                EXAMPLE_I2S_CHAN_NUM);
 
    // 1. 写入WAV头部
    if (m_file->write_file((char *)&wav_header,sizeof(wav_header))!=ESP_OK) {
        ESP_LOGE(TAG, "写入WAV头部失败");
        return ESP_FAIL;
    }
    
    // 2. 实时录音并分段写入数据
    /* Start recording */
    size_t wav_written = 0;
    static int16_t i2s_readraw_buff[8192];
     

    //使能通道,如果出错直接跳到err标签位置
    ESP_GOTO_ON_ERROR(audio_input->enable(), err, TAG, "error while starting i2s rx channel");

    while (wav_written < wav_size) {
        if(wav_written % byte_rate < sizeof(i2s_readraw_buff)) {
            ESP_LOGI(TAG, "Recording: %"PRIu32"/%ds", wav_written/byte_rate + 1, EXAMPLE_RECORD_TIME_SEC);
            printf(".");
        }
        size_t bytes_read = 0;
        /* Read RAW samples from ES7210 */
        ESP_GOTO_ON_ERROR(audio_input->read(i2s_readraw_buff, sizeof(i2s_readraw_buff), &bytes_read), err, TAG, "error while reading samples from i2s");
        /* Write the samples to the WAV file */
        // ESP_GOTO_ON_FALSE(m_file->write_file(filename,(char *)i2s_readraw_buff,true), ESP_FAIL, err,
        // TAG, "error while writing samples to wav file");


        if (m_file->write_file( (char *)i2s_readraw_buff,bytes_read)!=ESP_OK) {
            ESP_LOGE(TAG, "写入音频数据失败");
            goto err;
        }

        wav_written += bytes_read;
    } 

    printf("录制完成\n");
err:
    audio_input->disable();
    return ret;

}
 

WavRecorder 重构前后核心差异对比

维度 重构前(原有代码) 重构后(当前代码)
依赖对象 直接依赖 AudioEs7210 实例、I2S 句柄,耦合底层硬件 依赖 FileInterface(文件操作接口)+ AudioInputInterface(音频输入接口),仅依赖抽象,不碰具体实现
构造函数 仅接收文件操作对象,音频采集需外部传 I2S 句柄 同时接收文件接口和音频输入接口(智能指针注入),初始化时完成依赖绑定
录音核心逻辑 手动调用 i2s_channel_enable/disable/read,需处理 I2S 句柄、硬件错误 调用接口的 enable()/read()/disable(),无需知道底层是 ES7210/I2S,只关注 "启用 - 读数据 - 禁用" 逻辑
扩展性 换音频采集硬件(如其他麦克风芯片)/ 换存储介质(如闪存),需大幅修改录音逻辑 仅需新增对应接口的实现类,WavRecorder 代码无需改动(符合开闭原则)
代码职责 既管录音逻辑,又管硬件操作,职责混乱 只管 "生成 WAV 头 + 分段写数据",硬件操作 / 文件操作交给接口实现类,单一职责

差异 1:构造函数 ------ 新增音频输入接口注入(最核心)

cpp 复制代码
// 重构前:只接收文件接口,音频硬件完全靠外部传参
WavRecorder::WavRecorder(std::shared_ptr<FileInterface> fileInterface) 
: m_file(fileInterface) {} 

// 重构后:同时接收文件接口+音频输入接口,硬件操作能力内置
WavRecorder::WavRecorder(std::shared_ptr<FileInterface> fileInterface,std::shared_ptr<AudioInputInterface> audioInput) 
: m_file(fileInterface),audio_input(audioInput) {} 

旧代码是 "录音器只带了文件存储功能,录音功能得外接麦克风(传 I2S 句柄)";

新代码是 "录音器内置了通用麦克风接口,只要是符合接口的麦克风都能插,不用管麦克风型号"。

差异 2:record 函数 ------ 移除硬件句柄参数(调用更简单)

cpp 复制代码
// 重构前:必须传I2S句柄,上层要先初始化硬件、拿到句柄才能调用
esp_err_t WavRecorder::record(i2s_chan_handle_t i2s_chan, uint16_t seconds) {

// 重构后:只传录音时长,硬件细节全被接口屏蔽
esp_err_t WavRecorder::record( uint16_t seconds) {

旧代码调用:recorder.record(i2sChan, 10);(得先搞懂 I2S 句柄怎么来);

新代码调用:recorder.record(10);(只关心录 10 秒,其他都不用管)。

差异 3:硬件操作 ------ 从 "直接调驱动" 到 "调接口"(解耦核心)

cpp 复制代码
// 重构前:直接操作I2S驱动,强耦合硬件
ESP_GOTO_ON_ERROR(i2s_channel_enable(i2s_chan), err, TAG, "..."); // 启用I2S
ESP_GOTO_ON_ERROR(i2s_channel_read(i2s_chan, ...), err, TAG, "..."); // 读I2S数据
i2s_channel_disable(i2s_chan); // 禁用I2S

// 重构后:调用接口方法,和硬件解耦
ESP_GOTO_ON_ERROR(audio_input->enable(), err, TAG, "..."); // 启用音频输入(不管是ES7210还是其他芯片)
ESP_GOTO_ON_ERROR(audio_input->read(...), err, TAG, "..."); // 读音频数据(不管底层是I2S还是SPI)
audio_input->disable(); // 禁用音频输入(统一释放资源)

旧代码是 "你得亲手拆麦克风后盖、接电线才能录音";

新代码是 "你只需要按麦克风的'录音键',后盖怎么拆、电线怎么接,麦克风自己搞定"。

5.测试代码

cpp 复制代码
extern "C" void app_main(void)
{
    // 1. 创建SD卡文件操作对象(实现FileInterface接口)
    auto sdFile = std::make_shared<SdCard>();
    // 打开要写入的WAV文件(w+:可读可写,不存在则创建)
    ESP_ERROR_CHECK(sdFile->open("vvv.wav","w+"));

    // 2. 创建ES7210音频输入对象(实现AudioInputInterface接口)
    // 参数:采样率16000Hz,声道数2(立体声)
    auto audio = std::make_shared<AudioEs7210>(16000,2);

    // 3. 创建录音器,注入文件接口和音频输入接口(核心:依赖抽象)
    WavRecorder recorder(sdFile,audio); 
    
    // 4. 开始录音,仅传入时长(10秒),无需关心硬件细节
    recorder.record(10);

    // 5. 关闭文件,释放资源
    sdFile->close();
}

测试代码重构前后步骤对比

步骤 重构前(旧代码) 重构后(新代码) 差异本质
1 创建 SD 卡文件对象(sdFile) 创建 SD 卡文件对象(sdFile) 无变化,文件操作仍基于接口
2 打开 WAV 文件 打开 WAV 文件 无变化
3 创建WavRecorder,仅注入文件对象 创建WavRecorder,同时注入文件对象 + 音频输入对象 核心:依赖注入从 "单对象"→"双接口对象"
4 手动创建AudioEs7210对象 无此步骤(音频对象已在步骤 3 注入) 移除手动硬件对象创建
5 手动调用audio_es7210_init()获取 I2S 句柄 无此步骤(音频对象构造时自动初始化,句柄私有化) 移除硬件初始化 / 句柄获取
6 调用record,需传入 I2S 句柄 + 时长 调用record,仅传入时长 核心:硬件细节完全屏蔽
7 关闭文件 关闭文件 无变化

// 2. 创建ES7210音频输入对象(实现AudioInputInterface接口)

// 参数:采样率16000Hz,声道数2(立体声)

auto audio = std::make_shared(16000,2);

这一步相当于重构前:拆分在 "步骤 4(创建 AudioEs7210 对象)+ 步骤 5(手动初始化拿句柄)";

相关推荐
全栈技术负责人7 小时前
AI时代前端工程师的转型之路
前端·人工智能
亚里随笔7 小时前
GenEnv:让AI智能体像人一样在_游戏_中成长
人工智能·游戏·llm·rl·agentic
少林码僧8 小时前
2.29 XGBoost、LightGBM、CatBoost对比:三大梯度提升框架选型指南
人工智能·机器学习·ai·数据挖掘·数据分析·回归
喝拿铁写前端8 小时前
当 AI 会写代码之后,我们应该怎么“管”它?
前端·人工智能
春日见8 小时前
控制算法:PP(纯跟踪)算法
linux·人工智能·驱动开发·算法·机器学习
沫儿笙8 小时前
ABB焊接机器人混合气体节气方案
人工智能·机器人
余俊晖8 小时前
多页文档理解强化学习设计思路:DocR1奖励函数设计与数据构建思路
人工智能·语言模型·自然语言处理
Yeats_Liao8 小时前
MindSpore开发之路(二十六):系列总结与学习路径展望
人工智能·深度学习·学习·机器学习
sinat_286945198 小时前
opencode
人工智能·算法·chatgpt