MP3文件在生活中可以说非常熟悉了,几乎每天豆豆它本身是一种二进制文件,本篇文章就来看看它内部是如何编码的。
本项目用到的代码可以参考(其实核心的都在下边,最多不用移植了而已):
https://github.com/MY201314MY/Audio.git
一、基础知识
我们首先看几个与音频基础知识休戚相关的几个参数
采样频率
采样频率即一秒内的采样次数,它反映了采样点之间的间隔大小。间隔越小,丢失的信息越少,数字声音就越逼真细腻,要求的存储量也就越大。由于计算机的工作速度和存储容量有限,而且人耳的听觉上限为20kHz,所以采样频率不可能也不需要太高。根据奈奎斯特采样定律,只要采样频率高于信号中最高频率的两倍,就可以从采样中恢复原始的波形。因此,40kHz以上的采样频率足以使人满意。
测量精度
测量精度是样本在纵向方向的精度,是样本的量化等级,它通过对波形纵向方向等分而实现。由于数字化最终要用二进制表示,所以常用二进制数的位数表示样本的量化等级。若每个样本用8位二进制表示,则共有8个量级。若每个样本用16位二进制数表示,则共有16个量级。量级越多,采样越接近原始波形;数字声音质量越高,要求的存储也越大。目前多媒体计算机的标准采样量级有8位和16位两种。
声道数
声音记录只产生一个波形,称为单声道。声音记录只产生两个波形,称为立体声双道(最基本的立体声是两声道:左声道、右声道)。立体声比单声道声音丰满、空间感强,但需要两倍的存储空间。
比特率
比特率是指一秒采样时间内所包含的音频的数据流量,单位是bps。
二、MP3文件构成
MP3文件可以分成三部分:标签头(TAG_V2 ID3V2)、音频数据帧Frame、 TAG_V1(ID3V1),值得一提的是TAG_V2 ID3V2和TAG_V1(ID3V1)也是帧,可以称之为标签帧,Frame部分可以称之为数据帧。
在一个Mp3文件里,不一定有标签帧,但一定有数据帧。
ID3V2
包含了作者,作曲,专辑等信息,长度不固定,扩展了ID3V1的信息量。
Frame
这个数据帧非常重要真正发给声卡的的数据都是有这部分组成的。
它是由一系列的帧组成的,每个FRAME的长度可能不相等,也可能相等,由位率bitrate决定;每个FRAME又分为帧头和数据实体两部分。帧头记录了mp3的位率,采样率,版本等信息,每个帧之间相互独立。
ID3V1
ID3V1 是早期的版本,可以存放的信息量有限,但编程比 ID3V2 简单很多,即使到现在使用还是很多。 ID3V1 是固定存放在 MP3 文件末尾的 128 字节,歌名是固定分配为 30 个字节,如果歌名太短则以 0 填充完整,太长则被截断,其他信息类似情况存储。
换而言之我们把多个MP3文件的数据帧提取出来就可以连续播放了
连续播放多个MP3文件
嵌入式资源受限,我们将多个资源的音频预提取到RAM中,等播放的时候直接调用,节省读取文件的时间,因为RAM的读取速度比Flash快得多。
In PC(Windows)
在电脑上可运行成功后移植到板卡以提高效率,节省烧录时间,提出的依据可以看下文,或者参考
#include <stdio.h>
#include <stdint.h>
int main() {
char *array[] = {
"../audio/1.mp3", "../audio/bai.mp3", "../audio/5.mp3", "../audio/shi.mp3","../audio/3.mp3", "../audio/dian.mp3", "../audio/4.mp3", "../audio/2.mp3", "../audio/yuan.mp3"
};
int fileSize = 0;
uint8_t storeBuff[1024*1024];
FILE *output = fopen("output.mp3", "wb+");
for (uint8_t i=0;i<9;i++){
FILE *fp= fopen(array[i], "rb+");
fseek(fp, 0, SEEK_END);
int temp = ftell(fp);
printf("temp:%d\r\n", temp);
if(i != 8)
{
if(i==0) {
temp -= 128;
fseek(fp, 0, SEEK_SET);
fread((storeBuff + fileSize), temp, 1, fp);
}else{
temp -= 128+5672;
fseek(fp, 5672, SEEK_SET);
fread((storeBuff + fileSize), temp, 1, fp);
}
}else{
temp -= 5672;
fseek(fp, 5672, SEEK_SET);
fread((storeBuff+fileSize), temp, 1, fp);
}
fclose(fp);
fileSize += temp;
}
fwrite(storeBuff, fileSize, 1, output);
fclose(output);
return 0;
}
#### 1、标签帧
编码格式
```c
typedef struct
{
/* 必须为字符串"ID3",否则认为标签不存在 对应16进制:49 44 33 */
char Header[3];
/* 版本号 ID3V2.3就记录0x03 */
char ver;
/* 副版本号 */
char Revision;
/* 存放标志的字节 */
char Flag;
/* 标签帧的大小,不包括标签头的10个字节 */
char Size[4];
}head_lable;
MP3文件的开头就是这么定义的我们看一下我们手上的MP3文件
ubuntu@VM-16-10-ubuntu:~/Audio/search$ ls -l maria.mp3
-rw-rw-r-- 1 ubuntu ubuntu 7240 Apr 13 15:57 maria.mp3
#这个命令就是只看前32个字节,可以看到开头三个字节确实是 0x49 0x44 0x33
ubuntu@VM-16-10-ubuntu:~/Audio/search$ hexdump -C maria.mp3 -n 32
00000000 49 44 33 03 00 00 00 00 2c 1e 54 59 45 52 00 00 |ID3.....,.TYER..|
00000010 00 05 00 00 00 32 30 32 32 54 43 4f 4e 00 00 00 |.....2022TCON...|
00000020
我们重点看一下标签帧的的长度如何计算的:
第7-10个字节表示标签帧大小,一共四个字节,但每个字节只用7位,最高位不使用恒为0。所以格式如下,计算大小时要将0 去掉,得到一个28 位的二进制数,就是标签大小,计算公式如下:
int label_frame_size=((Size[0]&0x7F)<<21)
+ ((Size[1]&0x7F)<<14))
+ ((Size[2]&0x7F)<<7)
+ (Size[3]&0x7F);
这里的大小不包括标签头的10个字节,加上开始的10个字节就是5672个字节,也就是音频数据帧的偏移量。
至于标签帧中的其他信息是用来描述文件信息的,我们不做分析,这些描述信息占比:5672/7240=78.34%
2、数据帧
这个非常重要,也是我们需要或者重点查看的
#-s 表示偏移量
ubuntu@VM-16-10-ubuntu:~/Audio/search$ hexdump -C maria.mp3 -s 5672 -n 32
00001628 ff f2 49 c0 67 66 00 13 2a ba 25 9d 43 10 02 be |..I.gf..*.%.C...|
00001638 eb 6e 36 c2 26 5e fc 44 74 4d e0 44 4a 88 85 bb |.n6.&^.DtM.DJ...|
00001648
前边提到数据帧是由多个帧组成的,我们下边单独分析一下第一个帧:
帧头
typedef struct frameHeader
{
unsigned int sync1:11;
unsigned int version:2; //版本
unsigned int layer:2; //层
unsigned int crc_check:1; //CRC校验
unsigned int bit_rate_index:4; //比特率索引
unsigned int sample_rate_index:2; //采样率索引
unsigned int padding:1; //帧长调节位
unsigned int reserved:1; //保留字
unsigned int channel_mode:2; //声道模式
unsigned int mode_extension:2; //扩展模式,仅用于联合立体声
unsigned int copyright:1; //版权标志
unsigned int original:1; //原版标志
unsigned int emphasis:2; //强调方式
}FHEADER, *LPHEADER;
根据上边的链接和用hexdump查看得到的数据我们可以查询到两个非常关系的信息
- 采样数:在本帧中一共采集了多少个样点。
- 采样率:即采样频率,一秒内采集了多少次
- 比特率:一个样本用多大的数据位深表示
我们看一下如何获取这些参数:
首先要知道我们的版本:
- bit[13:12]=10 即 MPEG2
- bit[11:10]=01 即 Layer 3
由此得出比特率是32kbps,采样率是16kHz,采样数为576个/帧
MP3每帧长度计算
Size=((采样个数 * (1 / 采样率))* 帧的比特率)/8 + 帧的填充大小
除不尽取整数
对本例而言数据帧长度是:32000/(16000)x576/8 = 72*2=144
文件总大小为7240,ID3V2为5672,IDV31为128,数据帧总大小为7240-128-5672=1440
很神奇吧,也就是说本例有10个数据帧。
MP3每帧时长计算
每帧持续时间(毫秒) = 每帧采样数 / 采样频率 * 1000
对于本例而言
一帧时间 = 576/(16x1000)/(1000) = 36 ms
三、解码
目标:得到声卡可直接识别的PCM数据,PCM是很直接的二进制音频流
在这部分,本人看了很多资料,最终选择移植Helix库来解码,这个库在嵌入式MCU中被大量使用,下边对该库做简要说明:
感兴趣的读者可以到官网了解详细信息
Webset:Helix
解码细节设计大量数学计算,感兴趣的朋友自行了解,本文主要对格式进行说明。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "../pub/mp3dec.h"
int main() {
int16_t targetPCM[1024];
unsigned char buffer[10*1024] = {0};
memset(targetPCM, 0, sizeof (targetPCM));
/* 致敬Apple */
printf("This is designed by Apple in Cal.\n");
HMP3Decoder hmp3decoder = MP3InitDecoder();
/* 读取MP3源二进制文件 */
FILE *fp = fopen("../audio/maria.mp3", "rb+");
fseek(fp, 0, SEEK_END);
int count = ftell(fp);
fseek(fp, 0, SEEK_SET);
fread(buffer, count, 1, fp);
int offset = 0;
unsigned char *p = buffer;
int left = count;
static int k = 0;
while (1) {
/* 寻找同步帧0xFF,0xE0 */
offset = MP3FindSyncWord(p, count);
if(offset<0) {printf("offset:%d", offset);break;}
left -= offset;
p += offset;
count -= offset;
int err = MP3Decode(hmp3decoder, &p, &left, targetPCM, 0);
/* MP3Decode将解码后的PCM数据保存到targetPCM数组中,这个长度576的计算请看下一个章节 */
printf("left:%d\r\n", left);
for (int i = 0; i < 576; i++) {
if((i+1)%16 == 0)
printf(". ");
}
k++;
}
fclose(fp);
MP3FreeDecoder(hmp3decoder);
return 0;
}
解码后PCM长度的计算
/**************************************************************************************
* Function: MP3Decode
*
* Description: decode one frame of MP3 data
*
* Inputs: valid MP3 decoder instance pointer (HMP3Decoder)
* double pointer to buffer of MP3 data (containing headers + mainData)
* number of valid bytes remaining in inbuf
* pointer to outbuf, big enough to hold one frame of decoded PCM samples
* flag indicating whether MP3 data is normal MPEG format (useSize = 0)
* or reformatted as "self-contained" frames (useSize = 1)
*
* Outputs: PCM data in outbuf, interleaved LRLRLR... if stereo
* number of output samples = nGrans * nGranSamps * nChans
* updated inbuf pointer, updated bytesLeft
*
* Return: error code, defined in mp3dec.h (0 means no error, < 0 means error)
*
* Notes: switching useSize on and off between frames in the same stream
* is not supported (bit reservoir is not maintained if useSize on)
**************************************************************************************/
int MP3Decode(HMP3Decoder hMP3Decoder, unsigned char **inbuf, int *bytesLeft, short *outbuf, int useSize);
通过函数注释我们可以看到:PCM如果是双通道的话按照L-R的顺序排列,且默认就是16位,要求PCM数组足够大,长度是nGrans x nGranSamps x nChans,这个最终大小的参数可以通过*void MP3GetLastFrameInfo(HMP3Decoder hMP3Decoder, MP3FrameInfo *mp3FrameInfo)*获取即outputSamps,对于本MP3文件我们可以预览一下:
MP3GetLastFrameInfo(hmp3decoder, &frameInfo);
printf("bitrate:%d nChans:%d samprate:%d bitsPerSample:%d outputSamps:%d layer:%d version:%d\r\n",
frameInfo.bitrate,
frameInfo.nChans, frameInfo.samprate, frameInfo.bitsPerSample, frameInfo.outputSamps, frameInfo.layer,
frameInfo.version);
/* bitrate:32000 nChans:1 samprate:16000 bitsPerSample:16 outputSamps:576 layer:3 version:1 */
我们看一下输出文件
D:\project\CLion\Helix\cmake-build-debug\Helix.exe
This is designed by Apple in Cal.
left:1424
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:1280
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:1136
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:992
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:848
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:704
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:560
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:416
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:272
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . left:128
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . offset:-1
Process finished with exit code 0
Conclusion
解码10次,最后剩下128字节!
数据可视化
我们把生成的PCM数据打印出来并使用Python工具进行可视化分析
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "../pub/mp3dec.h"
int main() {
int16_t targetPCM[1024];
unsigned char buffer[10*1024] = {0};
memset(targetPCM, 0, sizeof (targetPCM));
printf("This is designed by Apple in Cal.\n");
HMP3Decoder hmp3decoder = MP3InitDecoder();
FILE *fp = fopen("../audio/maria.mp3", "rb+");
fseek(fp, 0, SEEK_END);
int count = ftell(fp);
fseek(fp, 0, SEEK_SET);
fread(buffer, count, 1, fp);
int offset = 0;
unsigned char *p = buffer;
int left = count;
static int k = 0;
while (1) {
offset = MP3FindSyncWord(p, count);
if(offset<0) {printf("offset:%d", offset);break;}
left -= offset;
p += offset;
count -= offset;
int err = MP3Decode(hmp3decoder, &p, &left, targetPCM, 0);
for (int i = 0; i < 576; i++) {
printf("i:%04d value:%04d\r\n", i+576*k, *(targetPCM + i));
}
/* 每个数据帧递增 */
k++;
}
fclose(fp);
MP3FreeDecoder(hmp3decoder);
return 0;
}
将打印出来的数据保存到文本文件output.txt,为方便理解,暂时不做二进制存储。
i:0796 value:-001
i:0800 value:-001
i:0816 value:-001
i:0823 value:-001
i:0837 value:-001
i:0838 value:-001
i:0853 value:-001
i:0902 value:-001
i:0921 value:0001
i:0923 value:-001
i:0925 value:0001
i:0931 value:0001
...
i:5742 value:-001
i:5745 value:-001
i:5747 value:-001
i:5749 value:-001
i:5752 value:-001
i:5755 value:0001
i:5759 value:-001
使用如下代码进行可视画分析:
import matplotlib.pyplot as plt
import numpy as np
time = np.zeros(shape=5760, dtype=np.int16)
value = np.zeros(shape=5760, dtype=np.int16)
with open(file="output.txt", mode="r") as f:
x = f.readlines()
#AXIS X
for i in range(5760):
time[i] = i
#AXIS Y
for item in x:
value[int(item[2:6])] = int(item[13:-1])
print("{0} -- {1}".format(item[2:6], item[13:-1]))
plt.figure(figsize=(16, 9))
plt.plot(time, value)
plt.show()
结果为
我们用成熟的波形软件做对比,可以看到波形是比较吻合的,这个软件是在线的,大家可以去官网看看
四、PCM封装为WAV音频格式并播放
PCM和WAV简介
WAV:wav是一种无损的音频文件格式,WAV符合 PIFF(Resource Interchange File Format)规范。所有的WAV都有一个文件头,这个文件头规定音频流的编码参数。
PCM:PCM(Pulse Code Modulation----脉冲编码调制)。所谓PCM编码就是将声音等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由1、0等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。PCM数据是最原始的音频数据完全无损。
简单来说:wav是一种无损的音频文件格式,pcm是没有压缩的编码方式,对pcm加头得到wav格式,该头的长度为44个字节。
对PCM加header
在此之前要先将上边的到文本转为PCM二进制文件
import numpy as np
import struct
time = np.zeros(shape=5760, dtype=np.int16)
value = np.zeros(shape=5760, dtype=np.int16)
with open(file="output.txt", mode="r") as f:
x = f.readlines()
for i in range(5760):
time[i] = i
for item in x:
value[int(item[2:6])] = int(item[13:-1])
with open(file="maria.pcm", mode="wb+") as pcm:
for i in range(5760):
pcm.write(struct.pack("h", value[i]))
Now!
import wave
pcmf = open("maria.pcm", 'rb')
pcmdata = pcmf.read()
pcmf.close()
print(pcmdata.__len__())
wavfile = wave.open("output.wav", 'wb')
#可以着重留意一下这几个参数
wavfile.setnchannels(1)
wavfile.setsampwidth(16//8)
wavfile.setframerate(16000)
wavfile.writeframes(pcmdata)
wavfile.close()
加下来就可以成功使用电脑的播放器播放得到的WAV文件
WAV header
我们看一下WAV文件头部44个字节分别表示什么意思:
可以参考这篇文章:
我们看一下我们的:
ubuntu@VM-16-10-ubuntu:hexdump -C output.wav -n 44
00000000 52 49 46 46 24 2d 00 00 57 41 56 45 66 6d 74 20 |RIFF$-..WAVEfmt |
00000010 10 00 00 00 01 00 01 00 80 3e 00 00 00 7d 00 00 |.........>...}..|
00000020 02 00 10 00 64 61 74 61 00 2d 00 00 |....data.-..|
0000002c
五、调频改变播放速度
MP3转PCM
由于之前的文件比较小,对播放速度调整后作用不明显,大量的打印对数据整体来说简直是灾难,我们先把MP3转为PCM二进制而不再是文本。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "../pub/mp3dec.h"
int main() {
int16_t targetPCM[10*1024];
MP3FrameInfo frameInfo;
unsigned char buffer[20*1024] = {0};
memset(targetPCM, 0, sizeof (targetPCM));
printf("This is designed by Apple in Cal.\n");
HMP3Decoder hmp3decoder = MP3InitDecoder();
FILE *fp = fopen("../audio/moon.mp3", "rb+");
fseek(fp, 0, SEEK_END);
int count = ftell(fp);
fseek(fp, 0, SEEK_SET);
fread(buffer, count, 1, fp);
int offset = 0;
unsigned char *p = buffer;
int left = count;
static int k = 0;
FILE *pcm = fopen("../audio/maria.pcm", "wb+");
while (1) {
offset = MP3FindSyncWord(p, count);
if(offset<0) {printf("offset:%d", offset);break;}
left -= offset;
p += offset;
count -= offset;
int err = MP3Decode(hmp3decoder, &p, &left, targetPCM, 0);
MP3GetLastFrameInfo(hmp3decoder, &frameInfo);
/* 这里直接按照二进制写入 */
fseek(fp, k*frameInfo.outputSamps*2, SEEK_SET);
fwrite(targetPCM, frameInfo.outputSamps*2, 1, pcm);
k++;
}
fclose(fp);
fclose(pcm);
MP3FreeDecoder(hmp3decoder);
return 0;
}
改变帧率以调速
然后我们对PCM加头得到WAV文件,更改framerate即可改变播放速度,但是声音也会发生很大的变化
速度尽量控制在0.8-1.2倍速之间,否则变化会很大,女生的声音可能为变为男生。
import wave
pcmf = open("maria.pcm", 'rb')
pcmdata = pcmf.read()
pcmf.close()
print(pcmdata.__len__())
wavfile = wave.open("output.wav", 'wb')
wavfile.setnchannels(1)
wavfile.setsampwidth(16//8)
/* 更改次参数可改变播放时长 帧率16K, 数据总长251136, 播放时长为251136/2/16000 = 7.848s */
wavfile.setframerate(16000)
wavfile.writeframes(pcmdata)
wavfile.close()
大家也可以用成熟的软件进行调速试播放查看效果
六、算法加快播放速度
当我们需要播放“一百二十三点24元”时,这段音乐实际上是由九个单独的音乐片段组成的,开头和结尾的静音导致音乐片段之间有明显的停顿,我们的目标是把文件开头和结尾静音过滤掉以保持原有音色的前提下提高播放速度。
算法一
这也是我目前想到的一个最简单、最实用的算法,大家如果感兴趣可自行尝试其他算法。
对比
不过滤,直接把多个MP3文件连接起来
- 这个wav文件大小为109484个字节
- 时长为3.419s
我们的思路很简单:就是把静音部分的样本去掉,既不改变音色,又能提高播报速度
我们看一下过滤后的波形
- 时长为2.092s
综上所述:
过滤算法文件尺寸(其实按PCM来算更合理一点,但是那个头大小固定的,就不做得那么准确的)减小了38.8%,播放时长减小了38.8%。
我么再来看一下代码,这个粗暴的办法把声音过滤掉的算法是非常不合理的,但是本人数学有比较差,其他的暂时没有想出来怎么弄。
#include <math.h>
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t buff[128*1024];
FILE *fp = fopen("../audio/he.pcm", "rb+");
FILE *output = fopen("../audio/maria.pcm", "wb+");
int16_t *p = (int16_t *)buff;
fseek(fp, 0, SEEK_END);
int temp = ftell(fp);
printf("temp:%d\r\n", temp/2);
fseek(fp, 0, SEEK_SET);
fread(buff, temp, 1, fp);
/* 这里样本大小大于100才会收集起来,我们可以自由控制这个阈值 */
for (int i=0;i<temp/2;i++)
{
if(abs(*(p+i)) >= 100){
fwrite(p+i, 2, 1, output);
}
}
printf("%2d\r\n");
fclose(fp);
fclose(output);
return 0;
}
注意
手机和电脑上播放音乐总是强制淡入淡出的,推荐你用这个在线剪辑软件播放。
其他算法
其他的我不会,请各位大佬补充吧。