【Arduino + Linux】基于 Helix 解码库实现 MP3 音频播放

3 篇文章 0 订阅

硬件连接方式:Arduino + Linux】基于NodeMCU32实现WAV音频播放 🚀
代码下载地址: https://github.com/npc-github-octocat/Helix_Mp3 🚀

======================================================================================================

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

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

一、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。

1.1、ID3V2.3

1.1.1、标签头

地址标识字节描述
00HHeader3字符串"ID3"
03HVer1版本号,ID3V2.3 就记录为3
04HRevision1副版本号,一般记录为0
05HFlag1标志字节,一般不用设置,位6指示该标签头后面是否有扩展标签头
06HSize4标签大小,整个ID3V2所占空间字节大小

标签大小计算:Total_size = (Size[0] & 0x7F) * 0x200000 + (Size[1] & 0x7F) * 0x400 + (Size[2] & 0x7F) * 0x80 + (Size[3] & 0x7F)

1.1.2、扩展标签头

扩展标签头(extended header)包含了一些对正确解析ID3v2标签信息影响不大的信息,可以它是可选的,结构依次为扩展标签头大小(Extended header size)、扩展标签头大小(Extended header size)、补白大小(Size of padding)。

  • 扩展标签头大小,现在可以是6或10个字节,不包括它自己。
  • 扩展标识最高位为1,那扩展标签头后就会有4个字节的CRC-32数据。
  • 补白大小就是整个ID3v2标签信息的大小减去标签头(header)和帧(frame)的大小。

1.1.3、标签帧

每个标签帧都有一个10字节的帧头和至少一个字节的不定长度内容,在文件中连续存放。

标签帧头由三个部分组成,即Frame[4]、Size[4] 和 Flag[2]。

  • Frame 是用四个字符表示帧内容含义,比如 TIT2 为标题、 TPE1 为作者、 TALB 为专辑、 TRCK 为音轨、 TYER 为年代等等信息。
  • Size 用四个字节组成 32bit 数表示帧大小。
  • Flags 是标签帧的标志位,一般为 0 即可。

1.2、音频数据

数据帧帧头:
在这里插入图片描述
位率位在不同版本和层都有不同的定义,具体参考表位率选择 ,单位为 kbps。其中 V1 对应MPEG-1, V2 对应 MPEG-2 和 MPEG-2.5; L1 对应 Layer1, L2 对应 Layer2, L3 对应 Layer3, free 表示位率可变, bad 表示该定义不合法。

在这里插入图片描述
数据帧长度取决于位率 (Bitrate) 和采样频率 (Sampling_freq),具体计算如下:

  • 对于 MPEG-1 标准, Layer1 时:Size=(48000*Bitrate)/Sampling_freq + Padding;
  • 对于 MPEG-1 标准, Layer2 或 Layer3 时:Size=(144000*Bitrate)/Sampling_freq + Padding;
  • 对于 MPEG-2 或 MPEG-2.5 标准, Layer1 时:Size=(24000*Bitrate)/Sampling_freq + Padding;
  • 对于 MPEG-2 或 MPEG-2.5 标准,Layer2 或 Layer3 时:Size=(72000*Bitrate)/Sampling_freq + Padding;

【WinHex软件查看MP3文件】这里以《嘉宾.mp3》为例:
在这里插入图片描述
FFFB9064转为二进制:1111 1111 1111 1011 1001 0000

蓝色:11位同步位
红色:11 - MPEG1
绿色:01 - Layer3
黑色:1 - 不校验
粉色:1001 - 位率查表可得 128kbps
紫色:00 - 采样频率 44.1kHz
金色:0 - 帧长不调整
橙色:0 - 保留字

根据计算公式可得:Size = (144000*128)/44100+0 = 417(向下取整)
上图中第一个框选的 FF 地址为1095,偏移417后,为1512,正好为第二个框选的 FF 地址(下一个数据帧)。

1.3、ID3V1

ID3V1 是早期的版本,可以存放的信息量有限,但编程比 ID3V2 简单很多,即使到现在使用还是很多。 ID3V1 是固定存放在 MP3 文件末尾的 128 字节,歌名是固定分配为 30 个字节,如果歌名太短则以 0 填充完整,太长则被截断,其他信息类似情况存储。

1.4、MP3文件结构图

在这里插入图片描述

参考:
[1]: 《STM32库开发实战指南——基于野火挑战者开发板》
[2]: MP3文件结构解析(超详细) 🚀
[3]: ID3v2 中文文档 (版本 2.3.0) 🚀
[4]: Mp3帧分析(数据帧) 🚀

二、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 数据帧,一次解码一帧,它是不能用来检索 ID3V1ID3V2 标签的,如果需要获取歌名、作者等信息需要自己编程实现。

三、Windows上播放MP3文件

开发工具:VS2019
开发语言:C
解码库:Helix 【下载地址:https://gitee.com/Microchip-MPLAB-Harmony/helix_mp3/tree/master/fixpnt 🚀】
声卡读写API: 该库中的 wave_out.hwave_out.c 文件【下载地址:speex中src目录下 🚀】

说明: 这里使用 Helix 解码库,如果直接从野火的案例中复制出来的(Helix源代码下载地址失效了),由于野火的版本是针对于 STM32 系列的,其编译器同Windows下的编译器不同,所以最后调用的一些底层函数存在差异,所以有部分文件是需要修改,删除arm目录下的文件,添加 polyphase.c、wave_out.h 以及 wave_out.c。

解码过程可能用到的 Helix 解码库函数有:

  • MP3InitDecoder: 初始化解码器函数,申请分配一个存储空间用于存放解码器状态的一个数据结构并将其初始化。
  • MP3FreeDecoder: 关闭解码器函数,释放由 MP3InitDecoder 函数申请的存储空间。
  • MP3FindSyncWord: 寻找帧同步函数,寻址数据帧开始的 11bit 都为“1” 的同步信息。
  • MP3Decode: 解码 MP3 帧函数,一次调用只解码一帧数据帧。
  • MP3GetLastFrameInfo: 获取帧信息函数。

3.1、伪代码分析

char readBuf[3000]; // 输入缓冲区,用于缓存待解码的数据帧,1940字节为最大MP3帧大小
short output[2304]; // 输出缓冲区,处理立体声音频数据时,输出缓冲区需要的最大大小为2304*16/8字节(16为PCM数据为16位)

char* mp3Path = "D:\\CloudMusic\\jiabin.mp3";      //系统盘(C:)下无法打开,这里方便起见就采用D盘

int main(){
	...	
	/* 初始化MP3解码器 */
	hMP3Decoder = MP3InitDecoder();
	/* 打开MP3文件 */
	mp3File = fopen(mp3Path, "rb");
	/* 设置声卡参数 */
	//44100到时候需要根据歌曲的实际采样率来填写,16位深度,双声道
	Set_WIN_Params(NULL, 44100, 16, 2);
	...
	/* 读取MP3文件到输入缓冲区 */
	int br = fread(readBuf, 1, READBUF_SIZE, mp3File);
	
	while(1){
		//寻找输入缓冲区中的同步位(11位1)
		offset = MP3FindSyncWord(readPtr, bytesLeft);

		if(没找到数据帧){
			mp3文件继续往下读,读入到输入缓冲区
		}
		else{ //找到数据帧
			//找到后将指针将指针定位到输入缓冲区数据帧起始位置,更新缓冲区有效数据
		    //如果有效数据小于某个值,那么可能存在输入缓冲区中数据帧的不完整,所以保险起见,这个值可以设的稍微大点。
			if(有效数据<1024){  // 野火版本中是1024,不是很懂,如果用1940也是正常的
				补充数据,继续从mp3文件中读入到输入缓冲区
			}
			
			//MP3Decode函数执行完成后,readPtr 和 bytesLeft 的值都会发生变化,指向下一帧数据
			int errs = MP3Decode(hMP3Decoder, &readPtr, &bytesLeft, output, 0);//调用一次解码一帧数据帧
			frames++; //记录数据帧数
			
			if(解码不正常){
				错误处理
			}
			else{ //解码正常
				WIN_Play_Samples(output, sizeof(short)*outputSamps); // 将解码完成的PCM数据发送给声卡,播放音频
			}
		}		
	}

	//判断是否正常读写结束
	if (fseek(mp3File, 0, SEEK_END) == 0) {
		printf("frames: %d\n", frames);
		printf("MP3解码完成\n");
	}

	MP3FreeDecoder(hMP3Decoder);
	WIN_Audio_close(); //关闭设备
	fclose(mp3File);   //关闭文件句柄

	return 0}

3.2、创建项目 & 编译生成可执行文件

① 创建新项目
在这里插入图片描述
② 空项目 ——> 下一步
在这里插入图片描述
③ 配置新项目 ——> 创建
在这里插入图片描述
④ 将相关文件添加到项目中

在这里插入图片描述
⑤ helix_mp3_for_windows 右键 ——> 属性 ——> C/C++ ——> 常规 ——> 附加包含目录中输入…/pub/ ——> 确定

在这里插入图片描述

⑥ helix_mp3_for_windows 右键 ——> 属性 ——> C/C++ ——> 预处理器 ——> 预处理器定义 ——> 点击右边向下箭头’ v ’ ——> 编辑 ——> 输入_CRT_SECURE_NO_DEPRECATE 以及 _CRT_SECURE_NO_WARNINGS ——> 确定。

在这里插入图片描述
⑦ helix_mp3_for_windows 右键 ——> 属性 ——> 链接器 ——> 输入 ——> 附加依赖项:点击右边向下箭头’ v ’ ——> 编辑 ——> 输入:winmm.lib (声卡所需要的库) ——> 确定 ——> 应用 ——> 确定。
在这里插入图片描述
现在可以运行了。

错误: C4996 ‘fopen’: This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS.

解决方法: 右键工程名->属性->C/C+±>预处理器->预处理器定义->编辑->添加_CRT_SECURE_NO_DEPRECATE
_CRT_SECURE_NO_WARNINGS

参考: https://blog.csdn.net/qq_34706266/article/details/88941500 🚀

四、ESP32上播放MP3文件

芯片平台、架构(Arduino、ESP_IDF等)、board的区别 🚀

开发板: NodeMCU32s(Esp32)
功放板: PAM8406 数字功放板
扬声器: 3 瓦 4 欧小喇叭

4.1、VS Code开发环境搭建

① 在扩展中查找 PlatformIO IDE,然后点击安装就可以了。

在这里插入图片描述

② 安装开发板所需要的固件。(据说5G流量下载比较快)

在这里插入图片描述

③ 填写板子信息,等待下载完成。

在这里插入图片描述

④ 在main.cpp中写LED闪烁程序,编译下载,如果成功的话,那环境算是配置完成了。

/* 本人使用的是 NodeMCU-32s 板子 */
#include <Arduino.h>

#define LED_PIN 2

void setup(){
    pinMode(LED_PIN, OUTPUT);       //初始化LED的GPIO
}

void loop(){
    digitalWrite(LED_PIN, HIGH);
    delay(1000); //延时1秒
    digitalWrite(LED_PIN, LOW);
    delay(1000); //延时1秒
}

在这里插入图片描述

说明:
这目录中有一个比较重要的文件,main.cpp不用说了,还有一个就是platformio.ini文件,当我们自己创建文件夹时,其它文件都可以不用,platformio.ini文件必须要,当打开工作文件夹时,软件根据该文件中的信息,配置编译下载环境,同时生成.pio用于保存编译生成的中间文件)以及.vscode记录了头文件、库文件等信息)。

没这个platformio.ini文件,下面没有编译下载按钮了。在这里插入图片描述

参考:
[1]: 基于 PlatformIO 平台玩转 NodeMCU 入门篇前言 🚀

4.2、将 Helix 移植到 ESP32 平台

编译环境: Platform IO IDF
架构: Arduino
编译器: xtensa-esp32-elf-gcc.exe

注意: 由于编译器参数未作修改,所以文件创建目录需要与每个目录规定方式进行分配,不然就会出现部分文件C程序不会进行编译,从而导致链接失败的情况出现。

项目文件结构

  • .pio:存放工程编译产生的文件。
  • .vscode:存放针对工程定制化的 vscode 配置文件。
  • include:存放统一管理的 h 头文件,其实头文件不一定要放到该目录下,只要在include时说明所在路径即可。
  • lib:存放自己编写的库文件。
  • src:存放工程项目的 C/C++ 源文件。

在这里插入图片描述

4.3、数据传输与音频播放

在完成 Helix 移植到 ESP32 平台后,之后需要做的就是基于 TCP/IP 的数据传输和 I2S 通信协议将数据发送到 DAC 上。

报错: fail on fd 54, errno: 104, “Connection reset by peer”
原因: 网络 send 的数据 size 太大。
参考: ConnectionResetError: [Errno 104] Connection reset by peer 🚀

由于解码产生的是16位数据,由于这里通过I2S的方式,利用 DMA 发送给内部DAC,DAC只会取8位数据进行输出,且这8位是16位数据中的高8位,因此,在完成解码后,需要将PCM16位转换成PCM8位,再将PCM8位放到高8位处,低 8 位置为0。
参考:
[1]: PCM8和PCM16互转 🚀
[2]: 8位PCM编码转换16位PCM 🚀
[3]: PCM音频处理(3)——格式转换 🚀

五、参考资料

[1]: Arduino教程 🚀
[2]: 【debugdump.com分享】VC2013使用helix解码mp3, 准备用于ESP32【1】 🚀
[3]: 【debugdump.com分享】VC2013使用helix解码mp3, 准备用于ESP32【2】 🚀
[4] ESP32- EPS32_SNOW AUDIO PLAY WAV和MP3播放(1) 🚀

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值