ESP32音频/语音技术(2)INMP441+MAX98357 本地 WAV 录音播放方案详解
一、软硬件环境说明硬件环境ESP32-S3INMP441MAX98357A软件环境Arduino 开发环境1. 硬件说明ESP32-S3-DevKitC-1 是一款入门级开发板搭载 Wi-Fi Bluetooth® LE 模组 ESP32-S3-WROOM-1、ESP32-S3-WROOM-1U 或 ESP32-S3-WROOM-2。参数项规格说明Flash 存储16MB用于存放固件与音频资源PSRAM8MB用于高速音频缓冲与数据处理核心架构双核 240MHz确保实时响应输入端 - INMP441 数字麦克风高灵敏度与数字信号直传特性降低噪声干扰。输出端 - MAX98357A D 类功放高效率驱动能力适合直接驱动扬声器。2. 依赖库安装依赖ESP32 内置driver/i2s.h、SPIFFS.h、Adafruit_NeoPixel库一键安装即可Arduino 库管理器搜索安装Adafruit NeoPixel作者 Adafruit安装方法Arduino IDE顶部菜单项目 → 加载库 → 管理库搜索Adafruit NeoPixel作者Adafruit点击安装建议安装最新稳定版3.ESP32-S3 N16R8开发板配置开发板ESP32S3 Dev ModuleFlash Size16MB (QIO)PSRAM8MB PSRAMUSB ModeUSB OTG二、硬件接线说明1. INMP441I2S0 输入录音INMP441ESP32-S3 GPIOSCK4WS5SD6VCC3.3VGNDGND2. MAX98357AI2S1 输出播放MAX98357ESP32-S3 GPIOBCLK15LRCLK16DIN7SD3.3V必须拉高使能VIN5VGNDGND关键三个模块共 GND否则电流噪声巨大MAX98357 SD 脚悬空无声。3. 板载外设BOOT 按键GPIO0无需外接WS2812GPIO48开发板自带三、程序功能说明1. 短按播放功能松手后触发播放当前recordN.wavWS2812 亮绿色播放完毕熄灭文件序号自动 15 之后回到 1 循环2. 长按录音功能按住 BOOT 键不放内部计时走到 500ms立刻启动录音红灯点亮只要手指不松开就一直录松开按键瞬间停止录音、保存 WAV、红灯熄灭保护限制最长连续录制 60 秒到点强制结束保存成功后文件序号 1满 5 个覆盖第一个3. LED 状态显示功能空闲全灭按住录音中红色常亮文件播放中绿色常亮4. 其他功能文件record1.wav ~ record5.wav循环覆盖每次录制 / 播放结束自动打印 SPIFFS 总容量、已用、剩余空间I2S 每次使用完毕卸载端口杜绝 ESP32-S3 内核 Guru 崩溃硬件接线完全沿用之前即可四、程序代码I2S0 硬件通道专门做录音 RX读取 INMP441 的 I2S PCM 数字音频I2S1 硬件通道专门做播放 TX推送数据给 MAX98357采样率 16kHz、16bit 单声道是语音最低开销标准环形数组做短时缓冲避免 S3 任务调度造成断音、卡顿纯硬件 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 APIdriver/i2s.h1. 关键 API 汇总函数作用i2s_driver_install注册初始化 I2S 硬件外设i2s_set_pinI2S 外设绑定 GPIO 引脚i2s_zero_dma_buffer清空 DMA 残留音频消除上电杂音i2s_readI2S 接收读取麦克风 PCM 数据i2s_writeI2S 发送推送 PCM 数据到功放i2s_driver_uninstall释放 I2S 驱动资源2. 两个核心结构体①i2s_config_tI2S 通用时序配置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_count4、dma_buf_len128ESP32 I2S 依靠DMA 搬运音频数据CPU 不需要逐字节搬运降低占用总 DMA 缓存采样4 × 128 512 个采样点作用平滑数据流防止 CPU 读写文件卡顿造成音频断音、爆音②i2s_pin_config_tI2S 引脚映射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);参数解释I2S_NUM_0指定 I2S 硬件端口audioBuf应用层软件缓冲区int16_t audioBuf [128]存放读到的 16bit PCM 采样sizeof(audioBuf)期望读取字节数readLen函数执行完成后实际读到的有效字节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);读取 SPIFFS 中 WAV 的 PCM 原始数据放入 audioBufi2s_write将内存 PCM 数据写入 I2S1 的 DMA 缓冲区I2S 硬件自动按照 16kHz 时序输出给 MAX98357 功放portMAX_DELAY永久阻塞等待缓冲区可写入播放场景保证音频不中断播放数据流链路SPIFFS WAV文件 → audioBuf软件缓存 → i2s_write → I2S1 DMA缓冲区 → I2S硬件 → MAX98357 → 扬声器6. I2S 资源释放i2s_driver_uninstall(I2S_NUM_0);录音 / 播放完成后卸载驱动释放 I2S 外设、DMA 内存避免多次重复安装驱动报错。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →