这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 微雪ESP32-P4开发板评测【四】解码MP3实现MP3音频播放

共1条 1/1 1 跳转至

微雪ESP32-P4开发板评测【四】解码MP3实现MP3音频播放

工程师
2025-12-14 00:47:51     打赏

简介

在上一篇文章中我们实现了PCM格式的音频播放和Echo (通过麦克风输入,然后扬声器播放),那么在本章节中我们将探究如何播放MP3格式的音乐。


视频效果


实际上无论是什么格式的音乐,在当前的开发板上过程都是对音频的解码和转换,然后写入I2S进行播放。流程图如下所示。

image.png

在不使用ESP-ADF的情况下, ESP组件管理器中也有很好的对MP3进行解码的库。比如说 chmorgan/esp-libhelix-mp3

image.png

由于MP3文件一般比较大,所以在开始之前最好先修改开发板的flash大小。首先修改为16MB

image.png

然后自定义分区表。

image.png

分配4MB的Flash空间给音频。

# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 0x500000,

然后在上一个章节的代码的最上方加上MP3 buffer的定义

/* Import MP3 file as buffer */
extern const uint8_t mp3_file_start[] asm("_binary_733711099_mp3_start");
extern const uint8_t mp3_file_end[] asm("_binary_733711099_mp3_end");


完成的解码和播放代码如下所示

/* MP3 player task */
static void i2s_mp3_player(void *args)
{
    ESP_LOGI(TAG, "[mp3] MP3 player start");

    // 初始化解码器
    HMP3Decoder mp3_decoder = MP3InitDecoder();
    if (!mp3_decoder)
    {
        ESP_LOGE(TAG, "[mp3] Failed to initialize MP3 decoder");
        vTaskDelete(NULL);
        return;
    }

    // 准备MP3数据
    const uint8_t *mp3_data = mp3_file_start;
    size_t mp3_size = mp3_file_end - mp3_file_start;
    size_t mp3_offset = 0;

    // 分配每次解码出来的PCM缓冲区
    //  Output PCM buffer (1 frame = 1152 samples per channel)
    int16_t *pcm_buffer = malloc(2 * 1152 * sizeof(int16_t));
    if (!pcm_buffer)
    {
        ESP_LOGE(TAG, "[mp3] Failed to allocate PCM buffer");
        MP3FreeDecoder(mp3_decoder);
        vTaskDelete(NULL);
        return;
    }

    ESP_LOGI(TAG, "[mp3] MP3 file size: %zu bytes", mp3_size);

    // 关闭I2S发送通道以进行预加载 MP3数据帧
    ESP_ERROR_CHECK(i2s_channel_disable(tx_handle));

    // First frame flag
    bool first_frame = true;
    int frame_count = 0;

    // 创建指针指向MP3数据
    unsigned char *mp3_ptr = (unsigned char *)mp3_data;

    // 循环解码MP3数据直到结束
    while (mp3_offset < mp3_size)
    {
        int bytes_left = mp3_size - mp3_offset;

        // 每次解码一个数据帧
        int err = MP3Decode(mp3_decoder,
                            &mp3_ptr,
                            &bytes_left,
                            pcm_buffer,
                            0); // 0 = don't skip ID3 tags

        if (err != 0)
        {
            if (err == ERR_MP3_INDATA_UNDERFLOW)
            {
                // End of file or need more data
                ESP_LOGI(TAG, "[mp3] Reached end of MP3 file");
                break;
            }
            else if (err == ERR_MP3_MAINDATA_UNDERFLOW)
            {
                // Frame skipped due to main data underflow, advance by 1 byte and try again
                mp3_ptr += 1;
                mp3_offset += 1;
                continue;
            }
            else
            {
                // Other error
                ESP_LOGW(TAG, "[mp3] Decode error: %d", err);
                mp3_ptr += 1;
                mp3_offset += 1; // Skip 1 byte and continue
                continue;
            }
        }

        // Get frame info to know how many samples we got
        MP3FrameInfo frame_info;
        MP3GetLastFrameInfo(mp3_decoder, &frame_info);

        if (frame_info.outputSamps > 0)
        {
            size_t pcm_bytes = frame_info.outputSamps * sizeof(int16_t);

            // 加载PCM数据到I2S发送通道(仅第一次解码时)
            if (first_frame)
            {
                size_t preload_bytes = 0;
                ESP_ERROR_CHECK(i2s_channel_preload_data(tx_handle, pcm_buffer,
                                                         pcm_bytes, &preload_bytes));
                ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
                first_frame = false;
                ESP_LOGI(TAG, "[mp3] First frame: samples=%d, bytes=%zu",
                         frame_info.outputSamps, pcm_bytes);
            }

            // 将解码后的数据帧发送给I2S
            size_t bytes_written = 0;
            esp_err_t ret = i2s_channel_write(tx_handle, pcm_buffer, pcm_bytes,
                                              &bytes_written, 1000);
            if (ret != ESP_OK)
            {
                ESP_LOGE(TAG, "[mp3] I2S write failed: %s", esp_err_to_name(ret));
                break;
            }

            frame_count++;
            if (frame_count % 10 == 0)
            {
                ESP_LOGI(TAG, "[mp3] Decoded %d frames, ~%zu ms played",
                         frame_count, (frame_count * 1152 * 1000) / frame_info.samprate);
            }
        }

        // Advance offset by consumed bytes (MP3Decode updates mp3_ptr)
        mp3_offset = mp3_ptr - (unsigned char *)mp3_data;
    }

    free(pcm_buffer);
    MP3FreeDecoder(mp3_decoder);
    ESP_LOGI(TAG, "[mp3] MP3 player finished - Total frames decoded: %d", frame_count);
    vTaskDelete(NULL);
}


这样的话解码器就会逐渐解码MP3文件,然后把数据帧发送给I2S 最后进行播放。使用MP3解码库非常方便,对于开发者而言并不需要关注具体的MP3的格式和细节。即可快速的完成功能的开发。


总结

本文主要在上一节 PCM 播放的基础上,引入了MP3 的解码流程,使用 esp-libhelix-mp3 在不依赖 ESP-ADF 的情况下实现了 MP3 音频播放。通过调整 Flash 分区、将 MP3 文件编译进固件并逐帧解码为 PCM 数据,再经 I2S 输出到音频设备,完整走通了 MP3 → PCM → I2S 的播放链路。实际上,借助成熟的解码库,开发者无需关心复杂的 MP3 格式细节,即可高效、稳定地完成嵌入式音频播放功能的实现。

04_I2SCodec.zip




关键词: ESP32-P4     MP3播放     ESP-IDF     E    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]