MP3PCMWAV等音频基础格式编码总结与代码分析
Posted M Friday
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MP3PCMWAV等音频基础格式编码总结与代码分析相关的知识,希望对你有一定的参考价值。
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)
在电脑上可运行成功后移植到板卡以提高效率,节省烧录时间,提出的依据可以看下文,或者参考
1、详细版,本人看了他的文章收获很大
2、精简版、包含代码实战
#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);
( 0 x 00 ) ∗ ( 1 < < 21 ) + ( 0 x 00 ) ∗ ( 1 < < 14 ) + ( 0 x 2 C ) ∗ ( 1 < < 7 ) + ( 0 x 1 e ) = 5662 / ∗ 对 我 们 的 文 件 来 说 ∗ / (0x00)*(1<<21)+(0x00)*(1<<14)+(0x2C)*(1<<7)+(0x1e) = 5662/* 对我们的文件来说 */ (0x00)∗(1<<21)+(0x00)∗(1<<14)+(0x2C)∗(1<<7)+(0x1e)=5662/∗对我们的文件来说∗/
这里的大小不包括标签头的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
. . . . . . . . . . . . . . . . . . .
二、GB2312
GB2312是对ASCII编码的中文扩展。为了表示汉字,决定不使用ASCII码的扩展字符集,而将128到255的内容进行重新编码,并用两个字节来表示汉字。因此0~127的意义不变,当两个大于128的字节放在一起时就表示一个中文,其中高字节用0xA1~0xF7编码,低字节用0xA1~0xFE。在这些编码中,还包括数字符号、罗马字母、希腊字母以及日语的假名。对于标点符号也用了两个字节编码,形成了全角符号。后来为了增大容量,同样要求高字节用大于127的字符,但后一个字节没有了限制,这种编码方法称为GBK编码,其保留了GB2312的所有码字匹配。
三、Unicode
由于不同字体符号各有各的编码方法,因此难于交流。ISO(国际标准化组织)决定解决这个问题,提出一种统一的编码方式。这种编码方法使用两个字节,也就是一个unicode字符占用两个字节。对于ASCII编码,Uincode保留了下来,并用两个字节来表示,因此高位字节为0。由于是重新编码,因此GBK与Unicode编码是不兼容的,必须通过查表进行转换。
四、UTF-8
由于计算机的兴起,需要一种编码方法能够对Unicode码进行高效的传输,因此就提出了UTF-8的编码方法,一次传输8位,其实Unicode编码的一种对应编码,编码方法如下:如果一个Unicode在0x0000-007F之间,则编码成0xxxxxxxx,其中x为原来的Unicode码,其它的编码方法为0x0080~0x07FF(Unicode)->110xxxxx 10xxxxxx(UTF-8),0x0800~0xFFFF(Unicode)->(1110xxxx 10xxxxxx 10xxxxxx)(UTF-8)。因此对于不同的Unicode编码,UTF-8用1个,2个或3个字节表示。
五、使用C++实现编码转换
static std::wstring MBytesToWString(const char* lpcszString);
static std::string WStringToMBytes(const wchar_t* lpwcszWString);
static std::wstring UTF8ToWString(const char* lpcszString);
static std::string WStringToUTF8(const wchar_t* lpwcszWString);
std::wstring KKLogObject::MBytesToWString(const char* lpcszString)//ascii码转unicode编码
{
int len = strlen(lpcszString);
int unicodeLen = ::MultiByteToWideChar(CP_ACP, 0, lpcszString, -1, NULL, 0);
wchar_t* pUnicode = new wchar_t[unicodeLen + 1];
memset(pUnicode, 0, (unicodeLen + 1) * sizeof(wchar_t));
::MultiByteToWideChar(CP_ACP, 0, lpcszString, -1, (LPWSTR)pUnicode, unicodeLen);
wstring wString = (wchar_t*)pUnicode;
delete [] pUnicode;
return wString;
}
std::string KKLogObject::WStringToMBytes(const wchar_t* lpwcszWString)//unicode转ascii编码
{
char* pElementText;
int iTextLen;
// wide char to multi char
iTextLen = ::WideCharToMultiByte(CP_ACP, 0, lpwcszWString, -1, NULL, 0, NULL, NULL);
pElementText = new char[iTextLen + 1];
memset((void*)pElementText, 0, (iTextLen + 1) * sizeof(char));
::WideCharToMultiByte(CP_ACP, 0, lpwcszWString, -1, pElementText, iTextLen, NULL, NULL);
std::string strReturn(pElementText);
delete [] pElementText;
return strReturn;
}
std::wstring KKLogObject::UTF8ToWString(const char* lpcszString)//utf-8转unicode
{
int len = strlen(lpcszString);
int unicodeLen = ::MultiByteToWideChar(CP_UTF8, 0, lpcszString, -1, NULL, 0);
wchar_t* pUnicode;
pUnicode = new wchar_t[unicodeLen + 1];
memset((void*)pUnicode, 0, (unicodeLen + 1) * sizeof(wchar_t));
::MultiByteToWideChar(CP_UTF8, 0, lpcszString, -1, (LPWSTR)pUnicode, unicodeLen);
wstring wstrReturn(pUnicode);
delete [] pUnicode;
return wstrReturn;
}
std::string KKLogObject::WStringToUTF8(const wchar_t* lpwcszWString)//unicode转utf-8
{
char* pElementText;
int iTextLen = ::WideCharToMultiByte(CP_UTF8, 0, (LPWSTR)lpwcszWString, -1, NULL, 0, NULL, NULL);
pElementText = new char[iTextLen + 1];
memset((void*)pElementText, 0, (iTextLen + 1) * sizeof(char));
::WideCharToMultiByte(CP_UTF8, 0, (LPWSTR)lpwcszWString, -1, pElementText, iTextLen, NULL, NULL);
std::string strReturn(pElementText);
delete [] pElementText;
return strReturn;
}
以上是关于MP3PCMWAV等音频基础格式编码总结与代码分析的主要内容,如果未能解决你的问题,请参考以下文章