ESP32使用I2S与MAX98357A从SD卡播放WAV文件的问题排查
解决方案:ESP32 NFC触发切换WAV播放问题
一、修复ESP32-audioI2S库的切换播放问题
你遇到的并非库不支持在setup()外调用播放,而是切换文件时未正确终止当前播放流程,且未避免重复触发。只需在切换前停止当前播放,并添加触发控制逻辑即可解决:
#include <Audio.h> #include <FS.h> #include <SD.h> #define I2S_DOUT 25 #define I2S_BCLK 27 #define I2S_LRC 26 Audio audio; bool nfcDetected = false; String targetAudioPath = ""; void setup() { Serial.begin(9600); SPI.begin(); if (!SD.begin()) { Serial.println("SD Card Mount Failed"); return; } audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); audio.setVolume(20); } void loop() { // 替换为你的NFC检测逻辑,检测到卡片时设置目标文件路径 detectNFC(); if (nfcDetected && !targetAudioPath.isEmpty()) { // 先停止当前播放,释放资源 audio.stop(); // 连接并播放新文件 if (!audio.connecttoFS(SD, targetAudioPath.c_str())) { Serial.println("Failed to load target audio file"); } // 重置触发标志,避免重复执行 nfcDetected = false; targetAudioPath = ""; } // 必须持续调用此函数,处理音频解码与输出 audio.loop(); } void detectNFC() { // 示例:从NFC模块读取卡片值,生成对应音频路径 String cardValue = getNFCValue(); // 替换为你的NFC读取函数 if (!cardValue.isEmpty()) { targetAudioPath = "/audios/" + cardValue + ".wav"; nfcDetected = true; } } // 模拟NFC读取函数,实际替换为你的模块代码 String getNFCValue() { // 这里返回检测到的卡片标识,比如"me llamo joaquin" return "me llamo joaquin"; }
核心要点:
- 切换前调用
audio.stop()终止当前播放,确保新的connecttoFS()能正确初始化 - 添加触发标志
nfcDetected,避免loop()中重复执行切换操作 audio.loop()必须持续调用,它负责处理音频解码、输出的后台逻辑
二、修复原生I2S驱动的倍速播放问题
倍速播放的核心原因是未跳过WAV文件头部数据,且I2S配置与实际音频参数不匹配。WAV头部包含采样率、通道数等关键信息,必须先解析这些参数再配置I2S,仅播放有效音频数据:
#define I2S_DOUT 25 #define I2S_BCLK 27 #define I2S_LRC 26 // 简化版WAV文件头部结构体 typedef struct { char riff[4]; // "RIFF"标识 uint32_t fileSize; // 文件总大小-8 char wave[4]; // "WAVE"标识 char fmt[4]; // "fmt "子块标识 uint32_t fmtSize; // 子块大小(PCM格式固定为16) uint16_t audioFormat; // 音频格式(1=PCM) uint16_t channels; // 通道数(1=单声道,2=立体声) uint32_t sampleRate; // 采样率 uint32_t byteRate; // 字节率 = 采样率*通道数*位深/8 uint16_t blockAlign; // 块对齐 = 通道数*位深/8 uint16_t bitsPerSample;// 位深(16位常用) char data[4]; // "data"子块标识 uint32_t dataSize; // 音频数据总大小 } WavHeader; void setup() { Serial.begin(9600); SPI.begin(); if (!SD.begin()) { Serial.println("SD card not found"); while (1); } // 初始化I2S(采样率为临时值,后续根据WAV文件调整) i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 44100, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 1024, .tx_desc_auto_clear = true, }; i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRC, .data_out_num = I2S_DOUT, .data_in_num = -1, }; i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, &pin_config); // 若使用外部I2S解码模块,注释掉下面一行(DAC模式仅用于ESP32内置DAC输出) // i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN); } void loop() { // 替换为你的NFC读取逻辑 String cardValue = getNFCValue(); String filename = "/audios/" + cardValue + ".wav"; File file = SD.open(filename.c_str()); if (!file) { Serial.println("Failed to open file for reading"); delay(1000); return; } // 读取并解析WAV头部 WavHeader header; file.read((uint8_t*)&header, sizeof(WavHeader)); // 验证是否为有效PCM格式WAV文件 if (memcmp(header.riff, "RIFF", 4) != 0 || memcmp(header.wave, "WAVE", 4) != 0 || memcmp(header.fmt, "fmt ", 4) != 0 || header.audioFormat != 1) { Serial.println("Not a valid PCM WAV file"); file.close(); delay(1000); return; } // 根据WAV文件参数动态配置I2S i2s_set_sample_rates(I2S_NUM_0, header.sampleRate); // 调整通道格式 if (header.channels == 1) { i2s_set_channel_format(I2S_NUM_0, I2S_CHANNEL_FMT_ONLY_RIGHT); } else { i2s_set_channel_format(I2S_NUM_0, I2S_CHANNEL_FMT_RIGHT_LEFT); } // 定位到音频数据起始位置(跳过头部) file.seek(sizeof(WavHeader)); // 播放音频数据 const size_t buffer_size = 1024; uint8_t buffer[buffer_size]; size_t bytes_read; size_t bytes_written; while (file.available() && (bytes_read = file.read(buffer, buffer_size)) > 0) { // WAV的16位PCM是小端格式,I2S需要大端格式,转换字节序 for (size_t i = 0; i < bytes_read; i += 2) { uint16_t temp = ((uint16_t)buffer[i+1] << 8) | buffer[i]; buffer[i] = temp >> 8; buffer[i+1] = temp & 0xFF; } i2s_write(I2S_NUM_0, buffer, bytes_read, &bytes_written, portMAX_DELAY); if (bytes_written != bytes_read) { Serial.println("Partial data write to I2S"); break; } } file.close(); delay(1000); // 避免重复触发 } // 模拟NFC读取函数 String getNFCValue() { return "me llamo joaquin"; }
核心修复点:
- 解析WAV头部:动态获取音频采样率、通道数等参数,确保I2S配置与文件一致
- 跳过头部数据:仅播放WAV文件中的有效音频部分,避免输出非音频数据
- 字节序转换:将WAV的小端格式PCM数据转换为I2S要求的大端格式
- 通道格式匹配:根据WAV文件的单/立体声配置I2S通道模式
内容的提问来源于stack exchange,提问作者Joaquin
相关产品推荐
相关产品推荐

