AI小智硬件程序(四)
- SP32S3实现麦克风录音保存到SD卡
- 麦克风录音功能代码重构
-
- [1. 问题痛点(重构前)](#1. 问题痛点(重构前))
- 2.核心解决方案:定义抽象接口类
- [3. 适配底层实现:ES7210 类继承并实现接口](#3. 适配底层实现:ES7210 类继承并实现接口)
- [4. 简化上层调用:录音类依赖抽象接口](#4. 简化上层调用:录音类依赖抽象接口)
- 5.测试代码
链接: 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(手动初始化拿句柄)";