简介
在上一篇文章中我们实现了PCM格式的音频播放和Echo (通过麦克风输入,然后扬声器播放),那么在本章节中我们将探究如何播放MP3格式的音乐。
视频效果
实际上无论是什么格式的音乐,在当前的开发板上过程都是对音频的解码和转换,然后写入I2S进行播放。流程图如下所示。

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

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

然后自定义分区表。

分配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 格式细节,即可高效、稳定地完成嵌入式音频播放功能的实现。
我要赚赏金
