3. 播放MP3音频文件

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

编译与运行

  1. 在项目根目录执行 idf.py set-target esp32s3 设置芯片目标。
  2. 使用 idf.py menuconfig 确认SD卡SPI引脚配置与代码一致(若使用默认引脚可跳过)。
  3. 将 test.mp3(44.1kHz采样率、128kbps码率)复制到SD卡根目录。
  4. 执行 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。