MP3解码入门(基于libhelix)

主要参考资料:
【Arduino + Linux】基于 Helix 解码库实现 MP3 音频播放: https://blog.csdn.net/weixin_42258222/article/details/122640413
libhelix-mp3: https://github.com/ultraembedded/libhelix-mp3/tree/master

MP3(Moving Picture Experts Group Audio Layer III,MPEG Audio Layer 3),本身是一种音频编码方式,MPEG 音频文件是 MPEG 标准中的声音部分,根据 压缩质量 和 编码复杂程度 划分为三层,即Layer-1、Layer-2、Layer-3,分别对应MP1、MP2、MP3 这三种声音文件,层次越高,编码器越复杂,压缩率也越高,MP3 压缩率可达到 10:1 至 12:1。

MP3 是利用人耳对高频声音信号不敏感的特性(人耳可听的频率在20hz~20khz),将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比,保证信号不失真。这样一来就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。

一、MP3文件

MP3 文件大致分为3个部分:TAG_V2(ID3V2)、音频数据、TAG_V1(ID3V1)

ID3V1 和 ID3V2 是 MP3 文件中附加关于该 MP3 文件的歌手、标题、专辑名称、年代、风格等等信息。

  • ID3V2 是可选的,如果存在 ID3V2 那它必然存在在MP3文件起始位置,常用的 ID3V2.3 版本。ID3V2.3 标签由一个标签头和若干个标签帧或一个扩展标签头组成。扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧

  • 音频数据由一系列数据帧 (Frame) 组成,每个 Frame 包含一段音频的压缩数据,通过解码库解码即可得到对应 PCM 音频数据,就可以通过 I2S 发送到 DAC芯片播放音乐,按顺序解码所有帧就可以得到整个 MP3 文件的音轨。每个 Frame 由两部分组成,帧头和数据实体,Frame 长度可能不同,由位率决定。11 位 1 表示数据帧开始。

  • ID3V1 固定存放在 MP3 文件末尾,固定长度为 128 字节,以 TAG 三个字符开头,后面跟上歌曲信息。因为 ID3V1 可存储信息量有限,有些 MP3 文件添加了 ID3V2。

二、MP3 解码库

MP3文件是经过压缩算法压缩而存在的,为得到 PCM 信号,需要对MP3文件进行解码,解码过程大致为:比特流分析、霍夫曼编码、逆量化处理、立体声处理、频谱重排列、抗锯齿处理、IMDCT处理、子带合成、PCM输出。

现在合适在小型嵌入式控制器移植运行的有两个版本的开源 MP3 解码库,分别为 Libmad 解码库和 Helix 解码库,Libmad 是一个高精度 MPEG 音频解码库,而 Helix 解码库需要占用的资源比 Libmad 解码库更少,特别是 RAM 空间的使用。

这两个解码库都是以 一帧为解码单位 的,一次解码一帧,这在应用解码时是需要着重注意的。

Helix 解码库工程中,实现 MP3 文件解码,将解码输出的 PCM 数据通过 I2S 接口 发送到 WM8978 芯片(ADC/DAC)实现音乐播放。

WAV 格式可以直接将音频数据发送给 DAC 芯片,输出声音,而对于 MP3 格式而言,其在数据的存储上并不是直接存储,而是经过一定的压缩,所以要想实现音频播放,就需要将原先压缩的数据恢复成原先的PCM数据。因此,MP3需要先经过解码库(如Helix)解码后,才可得到“可直接”播放的音频数据。在硬件上不需要做改动。

Helix 解码库是用来解码 MP3 数据帧,一次解码一帧,它是不能用来检索 ID3V1 和 ID3V2 标签的,如果需要获取歌名、作者等信息需要自己编程实现。

三、libhelix-mp3库

这个库里的API我们调用就好了,下面是最常用的一些,
在libhelix-mp3/pub/mp3dec.h目录下。

3.1 API介绍

在这里插入图片描述
1. MP3InitDecoder:

HMP3Decoder MP3InitDecoder(void);

这个函数用于初始化MP3解码器,创建一个解码器实例,并返回一个句柄(HMP3Decoder),该句柄在后续的解码过程中被用来引用这个解码器实例。

2. MP3FreeDecoder:

void MP3FreeDecoder(HMP3Decoder hMP3Decoder);

此函数用于释放先前通过MP3InitDecoder创建的MP3解码器实例。它接受解码器句柄作为参数,并释放与之关联的所有资源。

3. MP3Decode:

int MP3Decode(HMP3Decoder hMP3Decoder, unsigned char **inbuf, int *bytesLeft, short *outbuf, int useSize);

这个函数是MP3解码的核心,它将MP3编码的数据(inbuf)解码成PCM格式的音频(outbuf)。inbuf是一个指向输入缓冲区的指针的指针,解码器会更新这个指针以指向未处理的输入数据。bytesLeft是一个指向整数的指针,表示输入缓冲区中剩余的字节数。useSize是输出缓冲区的大小,函数会返回解码的样本数。

4. MP3GetLastFrameInfo:

void MP3GetLastFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo);

此函数用于获取最近一次成功解码的MP3帧的信息,并将这些信息存储在mp3FrameInfo结构中。这可以包括帧的比特率、频率、层信息等。

5. MP3GetNextFrameInfo:

int MP3GetNextFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo, unsigned char *buf);

这个函数用于从给定的缓冲区(buf)中解析下一个MP3帧的信息,并将这些信息存储在mp3FrameInfo结构中。它返回一个整数值,表示是否成功获取帧信息。

6. MP3FindSyncWord:

int MP3FindSyncWord(unsigned char *buf, int nBytes);

此函数用于在给定的缓冲区(buf)中查找MP3流的同步字节(通常是"11111111"的二进制序列,表示一个新帧的开始)。nBytes是缓冲区的大小。函数返回一个整数值,指示是否找到了同步字节。

3.2 案例

// decode
    offset = MP3FindSyncWord(readptr, bytesleft);
    if (offset < 0) {
      ESP_LOGD(TAG, "[decode task] MP3FindSyncWord not found.");
      continue;
    }
    readptr += offset;
    bytesleft -= offset;

    mp3_err = MP3Decode(player->impl->mp3decoder, &readptr, &bytesleft,
                        output_buf, 0);
    if (ERR_MP3_NONE != mp3_err) {
      ESP_LOGE(TAG, "[decode task] MP3Decode failed with error code: %d",
               mp3_err);
      event = EVENT_STOP;
      xQueueSend(player->impl->decode_event_queue, &event, portMAX_DELAY);
      continue;
    }

    MP3GetLastFrameInfo(player->impl->mp3decoder, &frame_info);
    ESP_LOGD(TAG, "[decode task] frame_info.outputSamps: %d",
             frame_info.outputSamps);
    size_t data_size =
        frame_info.outputSamps * sizeof(int16_t) * frame_info.nChans;
    pcm_frame_t *pcm_frame = (pcm_frame_t *)heap_caps_malloc(
        sizeof(pcm_frame_t) + data_size, MEM_TYPE);
    if (NULL == pcm_frame) {
      ESP_LOGE(TAG, "[decode task] Malloc pcm frame failed.");
      continue;
    }
    pcm_frame->data = (void *)(pcm_frame + 1);
    pcm_frame->size = data_size;
    pcm_frame->samprate = frame_info.samprate;
    pcm_frame->bits = 16;
    pcm_frame->channels = frame_info.nChans;
    pcm_frame->samps = frame_info.outputSamps;
    memcpy(pcm_frame->data, output_buf, data_size);
### 关于 Helix 解码的实现教程 Helix 解码提供了一种高效的 MP3 解码方案,支持多种比特率和音频格式[^1]。以下是基于该解码的一个简单实现流程: #### 1. 初始化解码环境 在使用 Helix 解码之前,需要初始化解码器的相关参数。这通常涉及分配内存空间并设置必要的配置选项。 ```c #include "helix_decoder.h" // 创建解码上下文结构体 HelixDecoderContext* decoder_context; // 分配内存给解码上下文 decoder_context = helix_create_decoder(); if (!decoder_context) { printf("Failed to create decoder context.\n"); } ``` #### 2. 加载 MP3 数据流 为了进行逐帧解码,需加载 MP3 文件数据或将数据流读入缓冲区。 ```c #define BUFFER_SIZE (1 << 16) uint8_t buffer[BUFFER_SIZE]; size_t bytes_read; FILE* mp3_file = fopen("example.mp3", "rb"); if (!mp3_file) { printf("Failed to open MP3 file.\n"); } bytes_read = fread(buffer, 1, BUFFER_SIZE, mp3_file); fclose(mp3_file); ``` #### 3. 执行逐帧解码 Helix 解码以帧为单位处理 MP3 数据,因此可以循环调用其解码函数来逐步解析文件中的每一帧。 ```c int decoded_samples; short pcm_buffer[DECODER_MAX_OUTPUT_SAMPLES]; while ((decoded_samples = helix_decode_frame(decoder_context, buffer, bytes_read, pcm_buffer)) > 0) { // 将 PCM 数据传递到音频播放设备或其他后续处理模块 process_pcm_data(pcm_buffer, decoded_samples); // 更新缓冲区指针以便继续读取下一帧的数据 memmove(buffer, buffer + decoded_samples * sizeof(short), bytes_read - decoded_samples * sizeof(short)); bytes_read -= decoded_samples * sizeof(short); } ``` #### 4. 清理资源 完成解码后,释放分配的内存和其他资源。 ```c helix_destroy_decoder(decoder_context); ``` 以上是一个简单的 Helix 解码示例框架。需要注意的是,在实际应用中可能还需要考虑错误处理、多线程同步等问题。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值