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等音频基础格式编码总结与代码分析的主要内容,如果未能解决你的问题,请参考以下文章

常见编码格式总结,与代码的互相转换

获取视频文件格式信息的工具和方法

PCM和WAV音频格式的区别,以及python自动转换

视频格式与编码问题分析

wav音频文件解析读取 定点转浮点分析 幅值提取(C语言实现)

音频编码格式——AAC简介