开始前适用开发板:ESP32 系列 · 开发框架:ESP-IDF
文档类型:ESP-IDF 通用 API 教程,不绑定单一开发板。
板级例程见 热门例程详解。
项目概述
本教程将介绍如何使用ESP32开发板实现音频录制和播放功能。项目基于ESP-IDF框架,使用LVGL图形库创建用户界面,通过I2S接口与音频编解码器通信,实现录音和播放功能。
硬件要求
- ESP32开发板(KSDIY Korvo系列)
- ES8311音频编解码器(用于音频输出)
- ES7210音频编解码器(用于音频输入)
- 扬声器和麦克风
软件架构
项目主要包含以下几个部分:
- 用户界面:使用LVGL库创建简洁的用户界面
- 音频驱动:初始化I2S和音频编解码器
- 录音和播放:通过I2S接口读写音频数据
代码详解
1. 用户界面 (ui_record.c)
用户界面包含一个圆形按钮用于录音和播放,以及一个滑块用于调节音量。
void ui_record(void)
{
// 获取默认屏幕
static lv_obj_t *default_src;
default_src = lv_scr_act();
// 创建圆形按钮
lv_obj_t *btn = lv_btn_create(default_src);
lv_obj_set_size(btn, 100, 100);
lv_obj_set_style_radius(btn, 50, LV_STATE_DEFAULT); // 设置为圆形
lv_obj_align(btn, LV_ALIGN_CENTER, 0, -80);
lv_obj_add_flag(btn, LV_OBJ_FLAG_CHECKABLE); // 可切换状态的按钮
lv_obj_add_event_cb(btn, btn_cb, LV_EVENT_VALUE_CHANGED, NULL);
// 创建提示文本
lv_obj_t *label = lv_label_create(default_src);
lv_label_set_text(label, "Press to Record & Play");
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
lv_obj_align_to(label, btn, LV_ALIGN_OUT_BOTTOM_MID, 0, 40);
// 创建音量滑块
lv_obj_t *volume_slider = lv_slider_create(default_src);
lv_obj_set_width(volume_slider, 150);
lv_obj_set_ext_click_area(volume_slider, 20);
lv_slider_set_range(volume_slider, 0, 90);
lv_slider_set_value(volume_slider, 85, LV_ANIM_ON);
lv_obj_align(volume_slider, LV_ALIGN_CENTER, 0, 70);
lv_obj_add_event_cb(volume_slider, volume_slider_cb, LV_EVENT_VALUE_CHANGED, NULL);
// 创建音量显示标签
slider_label = lv_label_create(default_src);
lv_label_set_text(slider_label, "85%");
lv_obj_set_style_text_font(slider_label, &lv_font_montserrat_16, LV_STATE_DEFAULT);
lv_obj_align_to(slider_label, volume_slider, LV_ALIGN_OUT_LEFT_MID, -10, 0);
}
按钮回调函数用于切换录音和播放模式:
static void btn_cb(lv_event_t *event)
{
lv_obj_t *btn = (lv_obj_t *) event->target;
if (lv_obj_has_state(btn, LV_STATE_CHECKED)) {
// 按钮被按下,开始录音
mode = record;
} else {
// 按钮被释放,开始播放
mode = play;
}
}
音量滑块回调函数用于调节音量:
static void volume_slider_cb(lv_event_t *event)
{
static char buf[8];
lv_obj_t *slider = (lv_obj_t *) event->target;
int volume = lv_slider_get_value(slider);
esp_codec_dev_set_out_vol(output_dev_, volume); // 设置音量
sprintf(buf, "%d%%", volume);
lv_label_set_text(slider_label, buf); // 更新音量显示
}
2. 音频驱动初始化 (app_speech.c)
音频驱动初始化包括I2S和音频编解码器的配置:
void Codec_I2S_init(void)
{
// 创建双工通道(发送和接收)
CreateDuplexChannels(AUDIO_I2S_GPIO_MCLK, AUDIO_I2S_GPIO_BCLK, AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT, AUDIO_I2S_GPIO_DIN);
// 初始化编解码器
Init_codec();
// 设置PA引脚(功率放大器控制)
gpio_pad_select_gpio(PA_GPIO_NUM);
gpio_set_direction(PA_GPIO_NUM, GPIO_MODE_OUTPUT);
gpio_set_level(PA_GPIO_NUM, 1); // 打开功率放大器
}
I2S通道配置:
void CreateDuplexChannels(gpio_num_t mclk, gpio_num_t bclk, gpio_num_t ws, gpio_num_t dout, gpio_num_t din)
{
// 配置I2S通道
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 2,
.dma_frame_num = 240 * 3,
.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模式(用于发送)
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)16000,
.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,
.bclk = bclk,
.ws = ws,
.dout = dout,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
// 配置TDM模式(用于接收)
i2s_tdm_slot_mask_t slot_mask = I2S_TDM_SLOT0 | I2S_TDM_SLOT1 | I2S_TDM_SLOT2 | I2S_TDM_SLOT3;
i2s_tdm_config_t tdm_cfg = {
.clk_cfg = {
.sample_rate_hz = (uint32_t)16000,
.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 = slot_mask,
.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,
.bclk = bclk,
.ws = ws,
.dout = I2S_GPIO_UNUSED,
.din = din,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
// 初始化I2S通道
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle_, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_tdm_mode(rx_handle_, &tdm_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle_));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle_));
ESP_LOGI(TAG, "双工通道创建完成");
}
编解码器初始化:
static void Init_codec(void)
{
// 初始化I2S数据接口
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);
assert(data_if_ != NULL);
// 初始化输出编解码器(ES8311)
audio_codec_i2c_cfg_t i2c_cfg = {
.port = I2C_NUM_1,
.addr = ES8311_CODEC_DEFAULT_ADDR,
.bus_handle = touch_i2c_bus_,
};
out_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(out_ctrl_if_ != NULL);
gpio_if_ = audio_codec_new_gpio();
assert(gpio_if_ != NULL);
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 = GPIO_NUM_NC;
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);
assert(out_codec_if_ != NULL);
esp_codec_dev_cfg_t dev_cfg = {
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
.codec_if = out_codec_if_,
.data_if = data_if_,
};
output_dev_ = esp_codec_dev_new(&dev_cfg);
assert(output_dev_ != NULL);
// 初始化输入编解码器(ES7210)
i2c_cfg.addr = ES7210_CODEC_DEFAULT_ADDR;
in_ctrl_if_ = audio_codec_new_i2c_ctrl(&i2c_cfg);
assert(in_ctrl_if_ != NULL);
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);
assert(in_codec_if_ != NULL);
dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
dev_cfg.codec_if = in_codec_if_;
input_dev_ = esp_codec_dev_new(&dev_cfg);
assert(input_dev_ != NULL);
// 配置输入采样参数
esp_codec_dev_sample_info_t fs = {
.bits_per_sample = 16,
.channel = 4,
.channel_mask = ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0),
.sample_rate = (uint32_t)16000,
.mclk_multiple = 0,
};
ESP_ERROR_CHECK(esp_codec_dev_open(input_dev_, &fs));
ESP_ERROR_CHECK(esp_codec_dev_set_in_channel_gain(input_dev_, ESP_CODEC_DEV_MAKE_CHANNEL_MASK(0), 40.0));
// 配置输出采样参数
esp_codec_dev_sample_info_t fs_out = {
.bits_per_sample = 16,
.channel = 1,
.channel_mask = 0,
.sample_rate = (uint32_t)16000,
.mclk_multiple = 0,
};
ESP_ERROR_CHECK(esp_codec_dev_open(output_dev_, &fs_out));
ESP_ERROR_CHECK(esp_codec_dev_set_out_vol(output_dev_, 90));
ESP_LOGI(TAG, "音频设备初始化完成");
}
3. 录音和播放功能 (app_main.c)
主程序初始化并创建录音播放任务:
void app_main(void)
{
// 初始化NVS(非易失性存储)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 分配音频缓冲区(在SPIRAM中)
audio_buffer = heap_caps_malloc(BUF_SIZE, MALLOC_CAP_SPIRAM);
// 初始化LVGL图形库
ksdiy_lvgl_port_init();
if (ksdiy_lvgl_lock(0))
{
// 创建用户界面
ui_record();
ksdiy_lvgl_unlock();
}
// 创建录音播放任务
xTaskCreate(audio_record_task, "audio_record_task", 4096 * 5, NULL, 5, NULL);
}
录音和播放任务实现:
void audio_record_task(void *args)
{
static esp_err_t ret;
static size_t bytes_read = 0;
static size_t bytes_write = 0;
// 初始化音频编解码器
Codec_I2S_init();
while (1)
{
if (mode == record) // 录音模式
{
ESP_LOGI(TAG, "录音开始");
audio_index = 0;
// 循环读取音频数据直到缓冲区满或模式改变
while ((mode == record) && (audio_index < (BUF_SIZE - FRAME_SIZE)))
{
// 从I2S读取音频数据
ret = i2s_channel_read(rx_handle_, audio_buffer + audio_index, FRAME_SIZE, &bytes_read, 100);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "[echo] i2s读取失败");
abort();
}
audio_index += FRAME_SIZE;
}
if (mode == record)
mode = idle; // 录音完成,切换到空闲模式
ESP_LOGI(TAG, "录音结束");
}
else if (mode == play) // 播放模式
{
ESP_LOGI(TAG, "播放开始");
audio_total = audio_index; // 记录录音长度
audio_index = 0;
// 循环播放音频数据直到播放完毕或模式改变
while ((mode == play) && (audio_index < audio_total))
{
// 向I2S写入音频数据
ret = i2s_channel_write(tx_handle_, audio_buffer + audio_index, FRAME_SIZE, &bytes_write, 100);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "[echo] i2s写入失败");
abort();
}
audio_index += FRAME_SIZE;
}
if (mode == play)
mode = idle; // 播放完成,切换到空闲模式
ESP_LOGI(TAG, "播放结束");
}
else // 空闲模式
{
vTaskDelay((100) / portTICK_PERIOD_MS); // 延时等待
}
}
vTaskDelete(NULL);
}
关键参数说明
- 采样率:16kHz(16000Hz)
- 位宽:16位
- 缓冲区大小:
BUF_SIZE = 16000 * 20 * sizeof(uint8_t),约可录制20秒音频 - 帧大小:
FRAME_SIZE = 1600,每次读写的数据量
工作流程
- 初始化NVS、LVGL和音频缓冲区
- 创建用户界面(按钮和滑块)
- 创建录音播放任务
- 当按下按钮时,进入录音模式,从麦克风读取音频数据到缓冲区
- 当释放按钮时,进入播放模式,将缓冲区中的音频数据播放出来
- 通过滑块可以调节播放音量
总结
本项目展示了如何使用ESP32实现简单的录音和播放功能。通过I2S接口与音频编解码器通信,可以实现高质量的音频录制和播放。LVGL图形库提供了简洁直观的用户界面,使用户可以方便地操作录音和播放功能。
这个项目可以作为更复杂音频应用的基础,例如语音识别、音频处理等。通过修改采样率、位宽和通道数,可以适应不同的音频需求。