4. 录音并保存为WAV文件
任务目标
本案例使用ESP32-S3的I2S外设连接数字麦克风(如INMP441),完成一段固定时长(默认5秒)的音频采集,并将PCM数据封装为WAV文件写入SD卡。通过读取SD卡上生成的文件头信息,验证WAV格式的正确性。
环境与依赖
- 硬件:ESP32-S3开发板、INMP441 I2S麦克风、MicroSD卡模块(SPI模式)
- 软件:ESP-IDF v5.x(需启用
esp_sdmmc或esp_sdspi组件) - 接线:INMP441的SCK→GPIO_NUM_4,WS→GPIO_NUM_5,SD→GPIO_NUM_6;SD卡SPI:CS→GPIO_NUM_10,SCK→GPIO_NUM_12,MOSI→GPIO_NUM_11,MISO→GPIO_NUM_13
实现原理
ESP32-S3的I2S外设配置为RX模式接收麦克风数据。INMP441输出24位有符号PCM数据,I2S接口按32位slot接收(低24位有效)。录音完成后,将PCM数据前添加44字节WAV头(RIFF/FMT/data块),写入SD卡。WAV头中的采样率、位深、声道数必须与实际PCM数据严格一致,否则文件无法正常播放。
关键完整代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/i2s_std.h"
#include "esp_sdspi.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#define SAMPLE_RATE 16000
#define SAMPLE_BITS 16
#define NUM_CHANNELS 1
#define RECORD_SECONDS 5
#define DMA_BUF_COUNT 8
#define DMA_BUF_LEN 1024
static const char *TAG = "wav_recorder";
// 写入WAV文件头(44字节标准格式)
static void write_wav_header(FILE *f, uint32_t data_bytes) {
uint32_t byte_rate = SAMPLE_RATE * NUM_CHANNELS * SAMPLE_BITS / 8;
uint16_t block_align = NUM_CHANNELS * SAMPLE_BITS / 8;
fwrite("RIFF", 1, 4, f);
uint32_t riff_size = 36 + data_bytes;
fwrite(&riff_size, 4, 1, f);
fwrite("WAVE", 1, 4, f);
fwrite("fmt ", 1, 4, f);
uint32_t fmt_size = 16;
fwrite(&fmt_size, 4, 1, f);
uint16_t audio_format = 1; // PCM
fwrite(&audio_format, 2, 1, f);
fwrite(&NUM_CHANNELS, 2, 1, f);
fwrite(&SAMPLE_RATE, 4, 1, f);
fwrite(&byte_rate, 4, 1, f);
fwrite(&block_align, 2, 1, f);
fwrite(&SAMPLE_BITS, 2, 1, f);
fwrite("data", 1, 4, f);
fwrite(&data_bytes, 4, 1, f);
}
void app_main(void) {
// ---- 1. 初始化SD卡(SPI模式) ----
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
spi_bus_config_t bus_cfg = {
.mosi_io_num = 11,
.miso_io_num = 13,
.sclk_io_num = 12,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
ESP_ERROR_CHECK(spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA));
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = 10;
slot_config.host_id = host.slot;
sdmmc_card_t *card;
ESP_ERROR_CHECK(sdmmc_card_init(&host, &slot_config, &card));
ESP_ERROR_CHECK(sdmmc_card_init(&host, &slot_config, &card));
// 挂载FAT文件系统
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
ESP_ERROR_CHECK(esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot_config, &mount_config, &card));
// ---- 2. 初始化I2S RX ----
i2s_chan_handle_t rx_chan;
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = DMA_BUF_COUNT;
chan_cfg.dma_frame_num = DMA_BUF_LEN;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &rx_chan, NULL));
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = GPIO_NUM_4,
.ws = GPIO_NUM_5,
.dout = I2S_GPIO_UNUSED,
.din = GPIO_NUM_6,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
// INMP441需要BCK/WS反相,具体取决于硬件接线
std_cfg.gpio_cfg.invert_flags.bclk_inv = true;
std_cfg.gpio_cfg.invert_flags.ws_inv = true;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_chan, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_chan));
// ---- 3. 录音并写入WAV ----
const char *filepath = "/sdcard/rec.wav";
FILE *f = fopen(filepath, "wb");
if (!f) {
ESP_LOGE(TAG, "无法创建文件");
return;
}
// 先写入占位头(数据长度未知)
write_wav_header(f, 0);
int32_t *buf = malloc(DMA_BUF_LEN * sizeof(int32_t));
size_t bytes_read = 0;
uint32_t total_pcm_bytes = 0;
uint32_t samples_to_read = SAMPLE_RATE * RECORD_SECONDS;
ESP_LOGI(TAG, "开始录音 %d 秒...", RECORD_SECONDS);
while (total_pcm_bytes / 2 < samples_to_read) {
size_t bytes = 0;
esp_err_t ret = i2s_channel_read(rx_chan, buf, DMA_BUF_LEN * sizeof(int32_t), &bytes, pdMS_TO_TICKS(1000));
if (ret == ESP_OK && bytes > 0) {
// 将32位slot数据转换为16位PCM(取高16位)
int16_t *pcm = malloc(bytes / 2);
for (int i = 0; i < bytes / 4; i++) {
pcm[i] = (int16_t)(buf[i] >> 16);
}
fwrite(pcm, 1, bytes / 2, f);
total_pcm_bytes += bytes / 2;
free(pcm);
}
}
free(buf);
// 回填正确的数据长度
fseek(f, 0, SEEK_SET);
write_wav_header(f, total_pcm_bytes);
fclose(f);
ESP_LOGI(TAG, "录音完成,共 %u 字节PCM数据", total_pcm_bytes);
// ---- 4. 验证文件头 ----
f = fopen(filepath, "rb");
if (f) {
char riff[5] = {0};
uint32_t file_size = 0;
fread(riff, 1, 4, f);
fseek(f, 4, SEEK_SET);
fread(&file_size, 4, 1, f);
fclose(f);
ESP_LOGI(TAG, "验证: RIFF=%s, 文件大小字段=%u", riff, file_size);
if (strcmp(riff, "RIFF") == 0 && file_size == total_pcm_bytes + 36) {
ESP_LOGI(TAG, "WAV文件验证通过");
} else {
ESP_LOGE(TAG, "WAV文件验证失败");
}
}
// 卸载SD卡
esp_vfs_fat_sdcard_unmount("/sdcard", card);
}
运行与验证
- 使用
idf.py build flash monitor编译烧录并查看串口日志。 - 录音期间保持麦克风附近有声音输入(如说话或播放音乐)。
- 日志出现“录音完成”后,串口输出
验证: RIFF=RIFF, 文件大小字段=160044(5秒×16000Hz×2字节=160000字节数据+44字节头),并显示“WAV文件验证通过”。 - 将SD卡插入电脑,用播放器打开
rec.wav,应能听到录制的5秒音频。
通过条件:串口日志显示验证通过,且SD卡上的WAV文件可在电脑上正常播放。
常见问题
- 无声音或噪声大:检查I2S的BCK/WS是否反相(INMP441通常需要
bclk_inv=true和ws_inv=true),并确认麦克风供电为3.3V。 - SD卡挂载失败:确认SPI接线正确,SD卡格式化为FAT32,且
format_if_mount_failed=false时卡内已有有效文件系统。 - 录音时长不准:I2S读取是DMA批量传输,实际停止时间可能略有延迟,属正常现象。