3. 播放MP3音频文件
任务目标
在ESP32-S3上集成MP3解码库(libhelix-mp3),实现从SD卡读取MP3文件,解码后通过I2S接口输出音频。本案例使用ESP-IDF v5.x框架,通过SPI方式挂载SD卡,I2S以标准Philips模式驱动外部DAC(如MAX98357A)或板载Codec。
环境与依赖
- 硬件:ESP32-S3-DevKitC + SD卡模块(SPI连接)+ MAX98357A I2S音频放大模块
- 软件:ESP-IDF v5.2+,需在
idf.py menuconfig中启用SPI Flash/SD卡驱动和I2S驱动 - 组件:
helix-mp3解码库(通过IDF组件管理器添加),esp_codec_dev(可选,用于统一音频设备接口)
接线参考:SD卡CS→GPIO10,SCK→GPIO12,MOSI→GPIO11,MISO→GPIO13;I2S:BCK→GPIO4,WS→GPIO5,DATA→GPIO6。
核心实现
以下代码实现完整播放流程:挂载SD卡→打开MP3文件→流式解码→I2S输出。解码器使用libhelix的MP3InitDecoder接口,每次读取固定大小数据块送入解码器,解码后的PCM数据直接写入I2S。代码包含必要的错误处理和资源释放逻辑。
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_sd.h"
#include "driver/i2s_std.h"
#include "helix_mp3.h"
#define SD_CS_GPIO 10
#define SD_SCK_GPIO 12
#define SD_MOSI_GPIO 11
#define SD_MISO_GPIO 13
#define I2S_BCK_GPIO 4
#define I2S_WS_GPIO 5
#define I2S_DATA_GPIO 6
#define MP3_FILE_PATH "/sdcard/test.mp3"
#define MP3_FRAME_SIZE 4096
#define PCM_BUF_SIZE 8192
static const char *TAG = "mp3_player";
// I2S 配置(标准Philips模式,44.1kHz/16bit)
static void i2s_init(void) {
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = 6,
.dma_frame_num = 240,
.auto_clear = true,
};
i2s_chan_handle_t tx_chan;
i2s_new_channel(&chan_cfg, &tx_chan, NULL);
i2s_std_config_t std_cfg = {
.clk_cfg = {
.sample_rate_hz = 44100,
.clk_src = I2S_CLK_SRC_DEFAULT,
},
.slot_cfg = {
.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT,
.slot_bit_width = I2S_SLOT_BIT_WIDTH_16BIT,
.slot_mode = I2S_SLOT_MODE_STEREO,
.slot_mask = I2S_STD_SLOT_LEFT | I2S_STD_SLOT_RIGHT,
},
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCK_GPIO,
.ws = I2S_WS_GPIO,
.dout = I2S_DATA_GPIO,
.din = I2S_GPIO_UNUSED,
.invert_flags = {0},
},
};
i2s_channel_init_std_mode(tx_chan, &std_cfg);
i2s_channel_enable(tx_chan);
}
// SD卡挂载(SPI模式)
static void sd_init(void) {
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
spi_bus_config_t bus_cfg = {
.mosi_io_num = SD_MOSI_GPIO,
.miso_io_num = SD_MISO_GPIO,
.sclk_io_num = SD_SCK_GPIO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4000,
};
spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA);
sdspi_device_config_t slot_config = {
.host_id = host.slot,
.gpio_cs = SD_CS_GPIO,
.gpio_int = GPIO_NUM_NC,
.gpio_wp = GPIO_NUM_NC,
.gpio_cd = GPIO_NUM_NC,
};
esp_vfs_fat_sdspi_mount("/sdcard", &host, &slot_config, NULL);
}
void mp3_player_task(void *arg) {
// 打开MP3文件
FILE *fp = fopen(MP3_FILE_PATH, "rb");
if (!fp) {
ESP_LOGE(TAG, "Failed to open %s", MP3_FILE_PATH);
vTaskDelete(NULL);
return;
}
// 初始化解码器
void *decoder = MP3InitDecoder();
if (!decoder) {
ESP_LOGE(TAG, "MP3 decoder init failed");
fclose(fp);
vTaskDelete(NULL);
return;
}
uint8_t *mp3_buf = malloc(MP3_FRAME_SIZE);
int16_t *pcm_buf = malloc(PCM_BUF_SIZE);
if (!mp3_buf || !pcm_buf) {
ESP_LOGE(TAG, "Memory allocation failed");
free(mp3_buf);
free(pcm_buf);
MP3FreeDecoder(decoder);
fclose(fp);
vTaskDelete(NULL);
return;
}
size_t bytes_read = 0;
int pcm_ptr = 0;
int pcm_len = 0;
while (1) {
// 读取MP3数据块
bytes_read = fread(mp3_buf, 1, MP3_FRAME_SIZE, fp);
if (bytes_read == 0) break; // 文件结束
// 解码数据块
int offset = 0;
while (offset < bytes_read) {
int ret = MP3Decode(decoder, &mp3_buf[offset], bytes_read - offset,
pcm_buf + pcm_ptr, &pcm_len);
if (ret == ERR_MP3_NONE) {
// 成功解码一帧,写入I2S
size_t bytes_written = 0;
i2s_channel_write(tx_chan, pcm_buf + pcm_ptr, pcm_len * 2, &bytes_written, portMAX_DELAY);
pcm_ptr = 0;
offset += MP3GetFrameInfo(decoder, NULL, NULL, NULL);
} else if (ret == ERR_MP3_NEED_MORE_DATA) {
// 数据不足,跳出内层循环继续读文件
break;
} else {
// 跳过错误帧
offset++;
}
}
}
// 清理资源
free(mp3_buf);
free(pcm_buf);
MP3FreeDecoder(decoder);
fclose(fp);
ESP_LOGI(TAG, "Playback finished");
vTaskDelete(NULL);
}
void app_main(void) {
sd_init();
i2s_init();
xTaskCreate(mp3_player_task, "mp3_player", 8192, NULL, 5, NULL);
}
编译与运行
- 在项目根目录执行
idf.py set-target esp32s3设置芯片目标。 - 使用
idf.py menuconfig确认SD卡SPI引脚配置与代码一致(若使用默认引脚可跳过)。 - 将
test.mp3(44.1kHz采样率、128kbps码率)复制到SD卡根目录。 - 执行
idf.py build flash monitor编译烧录并查看串口日志。
验证方法
播放开始后,音频输出端应能听到MP3内容。串口日志无错误输出,文件播放结束后打印 Playback finished。若听不到声音,依次检查:I2S引脚连接是否正确、SD卡文件路径是否存在、MP3文件是否为标准格式(可先用PC播放器验证)。
常见问题
- 解码报错ERR_MP3_INDATA_UNDERFLOW:读取块太小,增大
MP3_FRAME_SIZE至8192。 - 声音断续:I2S DMA缓冲区不足,增加
dma_desc_num或dma_frame_num。 - SD卡挂载失败:检查SPI接线和CS引脚,确认SD卡格式为FAT32。