硬件连接方式:【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-1
、Layer-2
、Layer-3
,分别对应MP1、MP2、MP3 这三种声音文件,层次越高,编码器越复杂,压缩率也越高,MP3 压缩率可达到 10:1
至 12:1
。
MP3 是利用人耳对高频声音信号不敏感的特性,将时域波形信号转换成频域信号,并划分成多个频段,对不同的频段使用不同的压缩率,对高频加大压缩比(甚至忽略信号)对低频信号使用小压缩比,保证信号不失真。这样一来就相当于抛弃人耳基本听不到的高频声音,只保留能听到的低频部分,这样可得到很高的压缩率。
MP3 文件大致分为3个部分:TAG_V2(ID3V2)
、音频数据
、TAG_V1(ID3V1)
ID3V1 和 ID3V2 是 MP3 文件中附加关于该 MP3 文件的歌手、标题、专辑名称、年代、风格等等信息。采用小端存储的方式,即高位在高地址。
ID3V2.3
版本。ID3V2.3 标签由一个标签头和若干个标签帧或一个扩展标签头组成。扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。128
字节,以 TAG 三个字符开头
,后面跟上歌曲信息。因为 ID3V1 可存储信息量有限,有些 MP3 文件添加了 ID3V2。地址 | 标识 | 字节 | 描述 |
---|---|---|---|
00H | Header | 3 | 字符串"ID3" |
03H | Ver | 1 | 版本号,ID3V2.3 就记录为3 |
04H | Revision | 1 | 副版本号,一般记录为0 |
05H | Flag | 1 | 标志字节,一般不用设置,位6指示该标签头后面是否有扩展标签头 |
06H | Size | 4 | 标签大小,整个ID3V2所占空间字节大小 |
标签大小计算:Total_size = (Size[0] & 0x7F) * 0x200000 + (Size[1] & 0x7F) * 0x400 + (Size[2] & 0x7F) * 0x80 + (Size[3] & 0x7F)
扩展标签头(extended header)包含了一些对正确解析ID3v2标签信息影响不大的信息,可以它是可选的,结构依次为扩展标签头大小(Extended header size)、扩展标签头大小(Extended header size)、补白大小(Size of padding)。
每个标签帧都有一个10字节的帧头和至少一个字节的不定长度内容,在文件中连续存放。
标签帧头由三个部分组成,即Frame[4]、Size[4] 和 Flag[2]。
数据帧帧头:
位率位在不同版本和层都有不同的定义,具体参考表位率选择 ,单位为 kbps。其中 V1 对应MPEG-1, V2 对应 MPEG-2 和 MPEG-2.5; L1 对应 Layer1, L2 对应 Layer2, L3 对应 Layer3, free 表示位率可变, bad 表示该定义不合法。
数据帧长度取决于位率 (Bitrate) 和采样频率 (Sampling_freq),具体计算如下:
Size=(48000*Bitrate)/Sampling_freq + Padding;
Size=(144000*Bitrate)/Sampling_freq + Padding;
Size=(24000*Bitrate)/Sampling_freq + Padding;
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 地址(下一个数据帧)。
ID3V1 是早期的版本,可以存放的信息量有限,但编程比 ID3V2 简单很多,即使到现在使用还是很多。 ID3V1 是固定存放在 MP3 文件末尾的 128 字节,歌名是固定分配为 30 个字节,如果歌名太短则以 0 填充完整,太长则被截断,其他信息类似情况存储。
参考:
[1]: 《STM32库开发实战指南——基于野火挑战者开发板》
[2]: MP3文件结构解析(超详细)
[3]: ID3v2 中文文档 (版本 2.3.0)
[4]: 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
标签的,如果需要获取歌名、作者等信息需要自己编程实现。
开发工具:VS2019
开发语言:C
解码库:Helix
【下载地址:https://gitee.com/Microchip-MPLAB-Harmony/helix_mp3/tree/master/fixpnt 】
声卡读写API: 该库中的 wave_out.h
和 wave_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:
获取帧信息函数。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;
}
① 创建新项目
② 空项目 ——> 下一步
③ 配置新项目 ——> 创建
④ 将相关文件添加到项目中
⑤ 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
芯片平台、架构(Arduino、ESP_IDF等)、board的区别
① 在扩展
中查找 PlatformIO IDE
,然后点击安装就可以了。
② 安装开发板所需要的固件。(据说5G流量下载比较快)
③ 填写板子信息,等待下载完成。
④ 在main.cpp中写LED闪烁程序,编译下载,如果成功的话,那环境算是配置完成了。
/* 本人使用的是 NodeMCU-32s 板子 */
#include
#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 入门篇前言
编译环境: Platform IO IDF
架构: Arduino
编译器: xtensa-esp32-elf-gcc.exe
注意: 由于编译器参数未作修改,所以文件创建目录需要与每个目录规定方式进行分配,不然就会出现部分文件C程序不会进行编译,从而导致链接失败的情况出现。
项目文件结构
- .pio:存放工程编译产生的文件。
- .vscode:存放针对工程定制化的 vscode 配置文件。
- include:存放统一管理的 h 头文件,其实头文件不一定要放到该目录下,只要在include时说明所在路径即可。
- lib:存放自己编写的库文件。
- src:存放工程项目的 C/C++ 源文件。
在完成 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)