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);
}

运行与验证

  1. 使用idf.py build flash monitor编译烧录并查看串口日志。
  2. 录音期间保持麦克风附近有声音输入(如说话或播放音乐)。
  3. 日志出现“录音完成”后,串口输出验证: RIFF=RIFF, 文件大小字段=160044(5秒×16000Hz×2字节=160000字节数据+44字节头),并显示“WAV文件验证通过”。
  4. 将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批量传输,实际停止时间可能略有延迟,属正常现象。