ESP32-S3+INMP441+MAX98357 本地 WAV 录音播放方案详解

一、软硬件环境说明

硬件环境: ESP32-S3+INMP441+MAX98357A

软件环境:Arduino 开发环境

1. 硬件说明

ESP32-S3-DevKitC-1 是一款入门级开发板,搭载 Wi-Fi + Bluetooth® LE 模组 ESP32-S3-WROOM-1、ESP32-S3-WROOM-1U 或 ESP32-S3-WROOM-2。

|--------------|-------------------|
| 参数项 | 规格说明 |
| Flash 存储 | 16MB,用于存放固件与音频资源 |
| PSRAM | 8MB,用于高速音频缓冲与数据处理 |
| 核心架构 | 双核 240MHz,确保实时响应 |

输入端 - INMP441 数字麦克风

高灵敏度与数字信号直传特性,降低噪声干扰。

输出端 - MAX98357A D 类功放

高效率驱动能力,适合直接驱动扬声器。

2. 依赖库安装

依赖:ESP32 内置 driver/i2s.hSPIFFS.hAdafruit_NeoPixel 库(一键安装即可)

Arduino 库管理器搜索安装:Adafruit NeoPixel(作者 Adafruit)

安装方法(Arduino IDE)

  1. 顶部菜单:项目 → 加载库 → 管理库
  2. 搜索:Adafruit NeoPixel
  3. 作者:Adafruit
  4. 点击安装(建议安装最新稳定版)

3. ESP32-S3 N16R8开发板配置

  1. 开发板:ESP32S3 Dev Module
  2. Flash Size:16MB (QIO)
  3. PSRAM:8MB PSRAM
  4. USB Mode:USB OTG

二、硬件接线说明

1. INMP441(I2S0 输入录音)

|-------------|-------------------|
| INMP441 | ESP32-S3 GPIO |
| SCK | 4 |
| WS | 5 |
| SD | 6 |
| VCC | 3.3V |
| GND | GND |

2. MAX98357A(I2S1 输出播放)

|--------------|-------------------|
| MAX98357 | ESP32-S3 GPIO |
| BCLK | 15 |
| LRCLK | 16 |
| DIN | 7 |
| SD | 3.3V(必须拉高使能) |
| VIN | 5V |
| GND | GND |

关键:三个模块共 GND,否则电流噪声巨大;MAX98357 SD 脚悬空无声。

3. 板载外设

  • BOOT 按键:GPIO0(无需外接)
  • WS2812:GPIO48(开发板自带)

三、程序功能说明

1. 短按播放功能

  1. 松手后触发播放当前 recordN.wav
  2. WS2812 亮绿色,播放完毕熄灭
  3. 文件序号自动 + 1,5 之后回到 1 循环

2. 长按录音功能

  1. 按住 BOOT 键不放,内部计时走到 500ms立刻启动录音,红灯点亮
  2. 只要手指不松开就一直录,松开按键瞬间停止录音、保存 WAV、红灯熄灭
  3. 保护限制:最长连续录制 60 秒,到点强制结束
  4. 保存成功后文件序号 + 1,满 5 个覆盖第一个

3. LED 状态显示功能

  • 空闲:全灭
  • 按住录音中:红色常亮
  • 文件播放中:绿色常亮

4. 其他功能

  • 文件:record1.wav ~ record5.wav 循环覆盖
  • 每次录制 / 播放结束自动打印 SPIFFS 总容量、已用、剩余空间
  • I2S 每次使用完毕卸载端口,杜绝 ESP32-S3 内核 Guru 崩溃
  • 硬件接线完全沿用之前即可

四、程序代码

  1. I2S0 硬件通道专门做录音 RX读取 INMP441 的 I2S PCM 数字音频;

  2. I2S1 硬件通道专门做播放 TX推送数据给 MAX98357;

  3. 采样率 16kHz、16bit 单声道,是语音最低开销标准;

  4. 环形数组做短时缓冲,避免 S3 任务调度造成断音、卡顿;

  5. 纯硬件 I2S DMA 搬运,CPU 占用极低。

    #include <Arduino.h>
    #include <driver/i2s.h>
    #include <SPIFFS.h>
    #include <Adafruit_NeoPixel.h>

    // ===================== 硬件引脚 =====================
    #define I2S0_BCLK 4
    #define I2S0_WS 5
    #define I2S0_IN 6

    #define I2S1_BCLK 15
    #define I2S1_WS 16
    #define I2S1_OUT 7

    #define BOOT_KEY_PIN 0
    #define WS2812_PIN 48
    #define NUMPIXELS 1

    // 音频参数
    #define SAMPLE_RATE 16000
    #define BITS_PER_SAMPLE 16
    #define CHANNELS 1
    #define BUF_SAMPLES 128

    // 业务规则
    #define MAX_RECORD_NUM 5
    #define LONG_PRESS_THR 500 // 按住超过500ms判定为录音模式
    #define MAX_RECORD_DUR 60000 // 最长录音60秒(ms)

    // WAV文件头结构体
    typedef struct {
    char riff[4];
    uint32_t file_len;
    char wave[4];
    char fmt[4];
    uint32_t fmt_len;
    uint16_t audio_fmt;
    uint16_t channels;
    uint32_t sample_rate;
    uint32_t byte_rate;
    uint16_t block_align;
    uint16_t bits_per_sample;
    char data_tag[4];
    uint32_t data_len;
    } WavHeader;

    Adafruit_NeoPixel led(NUMPIXELS, WS2812_PIN, NEO_GRB + NEO_KHZ800);
    File wavFile;
    int16_t audioBuf[BUF_SAMPLES];
    uint8_t currentFileIndex = 1;

    // ===================== I2S 配置 =====================
    const i2s_config_t i2sCfgRx = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = BUF_SAMPLES,
    .use_apll = true,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
    };

    const i2s_pin_config_t pinCfgRx = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = I2S0_BCLK,
    .ws_io_num = I2S0_WS,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S0_IN
    };

    const i2s_config_t i2sCfgTx = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = BUF_SAMPLES,
    .use_apll = true,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0
    };

    const i2s_pin_config_t pinCfgTx = {
    .mck_io_num = I2S_PIN_NO_CHANGE,
    .bck_io_num = I2S1_BCLK,
    .ws_io_num = I2S1_WS,
    .data_out_num = I2S1_OUT,
    .data_in_num = I2S_PIN_NO_CHANGE
    };

    // ===================== 工具函数 =====================
    void setLed(uint8_t r, uint8_t g, uint8_t b) {
    led.setPixelColor(0, led.Color(r, g, b));
    led.show();
    }

    String getFileName(uint8_t idx) {
    return "/record" + String(idx) + ".wav";
    }

    void writeWavHeader(File &f, uint32_t dataLen) {
    WavHeader hdr;
    uint32_t byteRate = SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
    uint16_t blockAlign = CHANNELS * (BITS_PER_SAMPLE / 8);

    复制代码
     memcpy(hdr.riff, "RIFF", 4);
     hdr.file_len = 36 + dataLen;
     memcpy(hdr.wave, "WAVE", 4);
     memcpy(hdr.fmt, "fmt ", 4);
     hdr.fmt_len = 16;
     hdr.audio_fmt = 1;
     hdr.channels = CHANNELS;
     hdr.sample_rate = SAMPLE_RATE;
     hdr.byte_rate = byteRate;
     hdr.block_align = blockAlign;

    hdr.bits_per_sample = BITS_PER_SAMPLE;
    memcpy(hdr.data_tag, "data", 4);
    hdr.data_len = dataLen;

    f.write((uint8_t *)&hdr, sizeof(WavHeader));
    }

    void printSPIFFSInfo() {
    size_t totalBytes = SPIFFS.totalBytes();
    size_t usedBytes = SPIFFS.usedBytes();
    size_t freeBytes = totalBytes - usedBytes;

    Serial.println("==================== SPIFFS 空间 ====================");
    Serial.printf("总容量: %d (%.2f KB)\n", totalBytes, totalBytes / 1024.0);
    Serial.printf("已使用: %d (%.2f KB)\n", usedBytes, usedBytes / 1024.0);
    Serial.printf("剩余: %d (%.2f KB)\n", freeBytes, freeBytes / 1024.0);
    Serial.println("====================================================\n");
    }

    // 按住录音,松开停止,最长60秒
    uint32_t holdRecording(uint8_t fileIdx) {
    String filePath = getFileName(fileIdx);
    Serial.printf("开始录音流程,保存至 %s,最长60秒上限\n", filePath.c_str());

    setLed(255, 0, 0); // 红灯 录音中

    i2s_driver_install(I2S_NUM_0, &i2sCfgRx, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pinCfgRx);
    i2s_zero_dma_buffer(I2S_NUM_0);

    if (SPIFFS.exists(filePath)) {
    SPIFFS.remove(filePath);
    Serial.printf("覆盖旧文件: %s\n", filePath.c_str());
    }
    wavFile = SPIFFS.open(filePath, FILE_WRITE);
    if (!wavFile) {
    Serial.println("文件创建失败");
    i2s_driver_uninstall(I2S_NUM_0);
    setLed(0, 0, 0);
    return 0;
    }

    wavFile.seek(sizeof(WavHeader));
    uint32_t totalDataLen = 0;
    unsigned long startMs = millis();

    // 循环条件:按键仍按住 并且 未超60秒最大时长
    while (digitalRead(BOOT_KEY_PIN) == LOW && (millis() - startMs) < MAX_RECORD_DUR) {
    size_t readLen = 0;
    i2s_read(I2S_NUM_0, audioBuf, sizeof(audioBuf), &readLen, 10);
    if (readLen > 0) {
    wavFile.write((uint8_t *)audioBuf, readLen);
    totalDataLen += readLen;
    }
    }

    // 收尾保存WAV
    unsigned long recTime = millis() - startMs;
    wavFile.seek(0);
    writeWavHeader(wavFile, totalDataLen);
    wavFile.close();
    i2s_driver_uninstall(I2S_NUM_0);
    setLed(0, 0, 0);

    Serial.printf("录音结束,实际时长:%d ms,音频数据长度:%d Bytes\n", recTime, totalDataLen);
    printSPIFFSInfo();
    return recTime;
    }

    // 播放指定序号文件
    void playAudio(uint8_t fileIdx) {
    String filePath = getFileName(fileIdx);
    Serial.printf("播放文件: %s\n", filePath.c_str());

    if (!SPIFFS.exists(filePath)) {
    Serial.printf("文件 %s 不存在,跳过播放\n", filePath.c_str());
    return;
    }

    setLed(0, 255, 0);
    wavFile = SPIFFS.open(filePath, FILE_READ);
    if (!wavFile) {
    Serial.println("打开播放文件失败");
    setLed(0, 0, 0);
    return;
    }

    wavFile.seek(sizeof(WavHeader));
    i2s_driver_install(I2S_NUM_1, &i2sCfgTx, 0, NULL);
    i2s_set_pin(I2S_NUM_1, &pinCfgTx);
    i2s_zero_dma_buffer(I2S_NUM_1);

    while (wavFile.available()) {
    size_t rlen = wavFile.read((uint8_t *)audioBuf, sizeof(audioBuf));
    if (rlen > 0) {
    size_t wlen;
    i2s_write(I2S_NUM_1, audioBuf, rlen, &wlen, portMAX_DELAY);
    }
    }

    wavFile.close();
    i2s_driver_uninstall(I2S_NUM_1);
    setLed(0, 0, 0);
    Serial.println("播放完成\n");
    printSPIFFSInfo();
    }

    void setup() {
    Serial.begin(115200);

    led.begin();
    led.setBrightness(80);
    setLed(0, 0, 0);

    pinMode(BOOT_KEY_PIN, INPUT_PULLUP);

    if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS 挂载失败");
    } else {
    Serial.println("SPIFFS 挂载成功");
    printSPIFFSInfo();
    }
    }

    void loop() {
    // 检测按键按下
    if (digitalRead(BOOT_KEY_PIN) == LOW) {
    unsigned long pressStart = millis();
    bool isRecordMode = false;

    复制代码
     // 持续按住循环检测
     while (digitalRead(BOOT_KEY_PIN) == LOW) {
       unsigned long heldTime = millis() - pressStart;
       // 按住超过500ms,进入录音模式
       if (heldTime >= LONG_PRESS_THR && !isRecordMode) {
         isRecordMode = true;
         holdRecording(currentFileIndex);
       }
       delay(10);
     }
    
     // 松手后总按下时长
     unsigned long totalHold = millis() - pressStart;
    
     if (isRecordMode) {
       // 有效录音,序号轮转
       currentFileIndex++;
       if (currentFileIndex > MAX_RECORD_NUM) {
         currentFileIndex = 1;
       }
       Serial.printf("下次录音将保存至 record%d.wav\n\n", currentFileIndex);
     } else {
       // 按住不足500ms,短按 = 切换播放
       playAudio(currentFileIndex);
       currentFileIndex++;
       if (currentFileIndex > MAX_RECORD_NUM) {
         currentFileIndex = 1;
       }
       Serial.printf("下次播放将指向 record%d.wav\n\n", currentFileIndex);
     }

    }

    delay(20);
    }

五、 I2S 完整解析

适用:ESP-IDF Legacy I2S API,driver/i2s.h

1. 关键 API 汇总

|----------------------|---------------------|
| 函数 | 作用 |
| i2s_driver_install | 注册初始化 I2S 硬件外设 |
| i2s_set_pin | I2S 外设绑定 GPIO 引脚 |
| i2s_zero_dma_buffer | 清空 DMA 残留音频,消除上电杂音 |
| i2s_read | I2S 接收:读取麦克风 PCM 数据 |
| i2s_write | I2S 发送:推送 PCM 数据到功放 |
| i2s_driver_uninstall | 释放 I2S 驱动资源 |

2. 两个核心结构体

i2s_config_t I2S 通用时序配置(RX/_TX 分别定义)

复制代码
const i2s_config_t i2sCfgRx = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), // 主机模式 + I2S接收(麦克风)
    .sample_rate = SAMPLE_RATE,                           // 采样率 16000Hz
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,         // 每个采样16bit
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,          // 仅左声道(INMP441单声道输出)
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,    // 标准I2S格式
    .intr_alloc_flags = 0,                                // 中断默认配置
    .dma_buf_count = 4,                                   // DMA缓冲区数量 =4
    .dma_buf_len = BUF_SAMPLES,                            // 每个DMA缓冲区采样点数=128
    .use_apll = true,                                     // 使用APLL高精度时钟,音频采样更稳定,减少噪声、频偏
    .tx_desc_auto_clear = false,                           // RX接收:不自动清理发送描述符
    .fixed_mclk = 0
    };

const i2s_config_t i2sCfgTx = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), // 主机模式 + I2S发送(功放)
    .sample_rate = SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = 0,
    .dma_buf_count = 4,
    .dma_buf_len = BUF_SAMPLES,
    .use_apll = true,
    .tx_desc_auto_clear = true, // TX开启:发送完成自动清空DMA缓冲区,避免杂音残留
    .fixed_mclk = 0
    };

DMA 缓冲说明(音频缓冲核心)

dma_buf_count=4dma_buf_len=128

  • ESP32 I2S 依靠DMA 搬运音频数据,CPU 不需要逐字节搬运,降低占用
  • 总 DMA 缓存采样:4 × 128 = 512 个采样点
  • 作用:平滑数据流,防止 CPU 读写文件卡顿造成音频断音、爆音

i2s_pin_config_t I2S 引脚映射

复制代码
const i2s_pin_config_t pinCfgRx = {
    .mck_io_num = I2S_PIN_NO_CHANGE, // 不使用MCLK主时钟(INMP441无需MCLK)
    .bck_io_num = I2S0_BCLK,         // BCLK位时钟
    .ws_io_num = I2S0_WS,            // WS左右声道帧时钟
    .data_out_num = I2S_PIN_NO_CHANGE, // RX无数据输出引脚
    .data_in_num = I2S0_IN            // 麦克风数据输入引脚
};

TX 结构体相反:data_out_num指定输出引脚,data_in_num悬空

3. I2S 初始化流程

复制代码
// 1. 安装I2S0驱动,传入配置
i2s_driver_install(I2S_NUM_0, &i2sCfgRx, 0, NULL);
// 2. 绑定GPIO引脚到I2S硬件外设
i2s_set_pin(I2S_NUM_0, &pinCfgRx);
// 3. 清空DMA缓存残留数据,防止上电杂音
i2s_zero_dma_buffer(I2S_NUM_0);

⚠️ 本程序设计:每次录音开始才安装驱动,录音结束 i2s_driver_uninstall 卸载 优点:空闲释放外设资源;缺点:频繁安装卸载有微小耗时,适合低频录音场景,不适合持续实时对讲。 播放函数 playAudio 对 I2S_NUM_1 使用完全一致初始化流程。

4. I2S 音频采集

复制代码
size_t readLen = 0;
// 从I2S0 DMA缓冲区读取音频到内存数组 audioBuf
i2s_read(I2S_NUM_0, audioBuf, sizeof(audioBuf), &readLen, 10);

参数解释:

  1. I2S_NUM_0:指定 I2S 硬件端口
  2. audioBuf应用层软件缓冲区(int16_t audioBuf 128,存放读到的 16bit PCM 采样
  3. sizeof(audioBuf):期望读取字节数
  4. readLen:函数执行完成后,实际读到的有效字节
  5. 10:超时时间 (ms),超时未读到数据直接返回

采集数据流链路

INMP441麦克风 → I2S0硬件 → DMA环形缓冲区 → i2s_read → audioBuf(软件缓存) → SPIFFS写入文件

分层缓冲:硬件 DMA 缓冲 + 上层数组 audioBuf 二级缓冲,保障音频连续

5. I2S 音频播放

复制代码
size_t wlen;
i2s_write(I2S_NUM_1, audioBuf, rlen, &wlen, portMAX_DELAY);
  1. 读取 SPIFFS 中 WAV 的 PCM 原始数据放入 audioBuf
  2. i2s_write 将内存 PCM 数据写入 I2S1 的 DMA 缓冲区
  3. I2S 硬件自动按照 16kHz 时序输出给 MAX98357 功放
  4. portMAX_DELAY:永久阻塞等待缓冲区可写入(播放场景,保证音频不中断)

播放数据流链路:SPIFFS WAV文件 → audioBuf软件缓存 → i2s_write → I2S1 DMA缓冲区 → I2S硬件 → MAX98357 → 扬声器

6. I2S 资源释放

复制代码
i2s_driver_uninstall(I2S_NUM_0);

录音 / 播放完成后卸载驱动,释放 I2S 外设、DMA 内存,避免多次重复安装驱动报错。


相关推荐
bu_shuo1 小时前
使用ffmpeg快速截取一段视频
ffmpeg·音视频
城管不管1 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
智购科技自动贩卖机2 小时前
自动售货机硬件主控方案选型实战:单片机、树莓派、ESP32怎么选?
人工智能·单片机·嵌入式硬件·物联网·网络协议·yolo·架构
Erishen2 小时前
🤖 用 AutoGen 搭 PSE 三角色闭环:可重试、可追溯、可审计的多智能体框架
架构·开源·agent
CRMEB2 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
LHX sir3 小时前
HubPort vs Node-RED:同样是可视化编排,有什么不同
物联网·node-red·协议自动化
艾为电子3 小时前
【应用方案】电视沉浸式音频升级: 电视音频 awinic“芯片 + 算法” 一体化解决方案
算法·音视频
dong_junshuai3 小时前
每天一个开源项目#73 Munder Difflin:2.3K Star 的本地多Agent办公室
开源·github·agent
却道天凉_好个秋3 小时前
计算机网络:CDN
学习·音视频·cdn