MP3数据解析


最近在做一个音响的小项目,需要将mp3文件解码输出为pcm文件,慢慢了解到mp3文件格式以及对应解码方式,记录学习。

  1. Mp3文件结构

ID3帧

标签帧

数据帧

ID3帧:大部分从音乐网站上下载的文件都会有ID3帧,MP3文件开头为"ID3"(0x49 0x44 0x33)表示ID3存在。

结构:帧头+内容

struct IDV3
{
    char Header[3];    /*必须为“ID3”否则认为标签不存在*/
    char Ver;          /*版本号ID3V2.3 就记录3*/
    char Revision;     /*副版本号此版本记录为0*/
    char Flag;         /*标志字节,只使用高三位,其它位为0 */
    char Size[4];      /*标签大小*/
}mp3IDV3;

帧头总共十个字节,最后一个Size比较重要,四个字节全部去掉最高位之后计算出一个28位的大小,表示去掉10Byte帧头之后的标签大小,计算公式:

ID3size=(mp3IDV3.Size[0]&0x7F)*0x200000+(mp3IDV3.Size[1]&0x7F)*0x400+(mp3IDV3.Size[2]&0x7F)*0x80+(mp3IDV3.Size[3]&0x7F);

这里的大小一般表示去掉上面开始十个字节之后剩余的 ID3帧+标签帧的大小,如果解析不在意这些标签可以直接将指针偏移到ID3size+10地址处开始读取mp3的数据帧。

标签帧:存储各种标签信息

结构:帧头+内容

typedef struct TAG
{
    char Header[4];    /*必须为“ID3”否则认为标签不存在*/
    char Size[4];      /*标签大小*/
    char Flag[2];
}mp3TAG;

所有的标签帧帧头都是这个结构,也就是十个字节。

Header的类型有很多这里列出一部分:

TIT2 =标题

TPE1 =作者

TALB =专集

TRCK =音轨 格式:N/M        其中 N 为专集中的第 N 首,M 为专集中共 M 首,N 和 M 为 ASCII 码表示的数字

TYER =年代 是用 ASCII 码表示的数字

TCON=类型 直接用字符串表示

COMM=备注 格式:"eng\0 备注内容",其中 eng 表示备注所使用的自然语言

APIC=表示存储了一个图片,APIC帧的帧头也遵循标签帧的结构,后面的帧内容是一张图片,一般是jpg格式

 

Size就表示帧内容的大小(不包括帧头10Byte),计算公式为:

size=tag->Size[0]*0x100000000+tag->Size[1]*0x10000+tag->Size[2]*0x100+tag->Size[3];

这里不需要将最高位去掉,直接计算即可。

Flag一般存一些标志,虽然是2个字节,但是只有每个字节的高三位有定义:

abc00000 ijk00000

a -- 标签保护标志,设置时认为此帧作废

b -- 文件保护标志,设置时认为此帧作废

c -- 只读标志,设置时认为此帧不能修改

i -- 压缩标志,设置时一个字节存放两个 BCD 码表示数字

j -- 加密标志

k -- 组标志,设置时说明此帧和其他的某帧是一组

 

数据帧:存放的音乐数据。

数据帧帧头是4个字节,按位划分为以下内容:

typedef struct FrameHeader
{
unsigned int sync;                        //同步信息
unsigned int version;                      //版本
unsigned int layer;                           //
unsigned int error_protection;           // CRC校验
unsigned int bitrate_index;              //位率
unsigned int sampling_frequency;         //采样频率
unsigned int padding;                    //帧长调节
unsigned int private;                       //保留字
unsigned int mode;                         //声道模式
unsigned int mode_extension;        //扩充模式
unsigned int copyright;                           // 版权
unsigned int original;                      //原版标志
unsigned int emphasis;                  //强调模式
}FHEADER;

 

static int parse_mp3header(char* buff,FHEADER *header)
{
    if(buff[0]!=0xff){
        printf("input header data error!\n");
        return -1;
    }
    header->sync=(buff[0]<<3)+((buff[1]&0xe0) >> 5);
    header->version=(buff[1]&0x18) >> 3;
    header->layer=(buff[1]&0x06) >> 1;
    header->error_protection=(buff[1]&0x01) >> 0;
    header->bitrate_index=(buff[2]&0xf0) >> 4;
    header->sampling_frequency=(buff[2]&0x0c) >> 2;
    header->padding=(buff[2]&0x02) >> 1;
    header->private=(buff[2]&0x01) >> 0;
    header->mode=(buff[3]&0xc0) >> 6;
    header->mode_extension=(buff[3]&0x30) >> 4;
    header->copyright=(buff[3]&0x08) >> 3;
    header->original=(buff[3]&0x04) >> 2;
    header->emphasis=(buff[3]&0x03) >> 0;
    return 0;
}

解析出来包含上面这些内容,主要用到的有version(采样率)、layer、sampling_frequency采样率、mode(通道数),其中采样率和mode需要读取出来配置解码器和I2S。

 

  1. 标签帧解析

    因为最开始对这些标签帧没有需求,只是实现MP3数据帧的读取、解码、输出和播放,所以最初的设想是直接计算出ID3的size然后跳过中间所有的帧,直接读取数据帧:

    if(mp3IDV3.Header[0]=='I' && mp3IDV3.Header[1]=='D' && mp3IDV3.Header[2]=='3'){
        printf("ID3 exist!\n");
        printf("Header:%s\n",mp3IDV3.Header);
        printf("Header Ver=%x\n",mp3IDV3.Ver);
        printf("Header ReVer=%x\n",mp3IDV3.Revision);
        printf("Header Flag=%x\n",mp3IDV3.Flag);
        printf("Header size=%x %x %x %x\n",mp3IDV3.Size[0],mp3IDV3.Size[1],mp3IDV3.Size[2],mp3IDV3.Size[3]);
        /* pointer move to next tag*/
        fileptr=fileptr+sizeof(mp3IDV3);
        ID3size=(mp3IDV3.Size[0]&0x7F)*0x200000+(mp3IDV3.Size[1]&0x7F)*0x400+(mp3IDV3.Size[2]&0x7F)*0x80+(mp3IDV3.Size[3]&0x7F)+sizeof(mp3IDV3);
        printf("Header size with 10 Bytes=%x\n",ID3size);
        printf("file ptr=%x\n",fileptr);

    }
    else{
        printf("ID3 not exist!\n");
    }

这里使用的从某云音乐软件下载的MP3文件解析:

ID3 exist!

Header:ID3

Header Ver=3

Header ReVer=0

Header Flag=0

Header size=0 0 c 3c

Header size with 10 Bytes=646

计算出标签帧大小为0x646:

 

可以看到0x646是数据帧帧头位置,后面直接开始解码也没有错误。

但是在尝试其他MP3文件时出现了错误,解码器一直接收不到正确的数据帧导致buffer溢出。

ID3 exist!

Header:ID3

Header Ver=3

Header ReVer=0

Header Flag=0

Header size=0 d 6 23

Header size with 10 Bytes=372d

找到对应的地址发现并不是数据帧

往前翻发现这里还处在APIC的图片数据中,前后计算发现ID3帧头中的size并不能明确指向某些数据的大小,于是有添加了帧头解析函数;

static tag_size parse_tag(mp3TAG *tag)
{
    tag_size size=0;
    if(tag->Header[0]!='C' && tag->Header[0]!='T' && tag->Header[0]!='A'){
        printf("no tag frame!\n");
        return 0;
    }
    printf("%s:",tag->Header);
    size=tag->Size[0]*0x100000000+tag->Size[1]*0x10000+tag->Size[2]*0x100+tag->Size[3];
    printf("tag size without header=%x * ", size);
    printf("flag=%x %x\n",tag->Flag[0],tag->Flag[1]);
    return size+sizeof(mp3TAG);
}

 

      do{
            f_read(&fmp3, &readtag, sizeof(readtag), &fnum);
            temp_ret=parse_tag(&readtag);
            fileptr=fileptr+temp_ret;
            f_lseek(&fmp3,fileptr);
        }while(temp_ret);

 

ID3 exist!

Header:ID3

Header Ver=3

Header ReVer=0

Header Flag=0

Header size=0 d 6 23

Header size with 10 Bytes=372d

file ptr=a

TSSE:tag size without header=e * flag=0 0

APIC:tag size without header=33f01 * flag=0 0

COMM:tag size without header=247 * flag=0 0

TALB:tag size without header=27 * flag=0 0

TIT2:tag size without header=33 * flag=0 0

TPE1:tag size without header=7 * flag=0 0

TPE2:tag size without header=7 * flag=0 0

no tag frame!

这样就可以顺利跳到标签帧的结尾,也为以后解析标签帧留一个接口。但是实际上标签帧的结尾到第一帧数据帧的帧头中间还有一些空白数据:

于是又添加了一个同步函数:

static tag_size sync_frame(char *buff,int len)
{
    tag_size size=0;
    for(size;size){
        if(buff[size]==0xff)
            break;
    }
    return size;
}

 

do{
        f_read(&fmp3,ReadBuffer,sizeof(ReadBuffer),&fnum);
        temp_ret=sync_frame(ReadBuffer,sizeof(ReadBuffer));
        fileptr=fileptr+temp_ret;
    }while(temp_ret==sizeof(ReadBuffer) && fnum==sizeof(ReadBuffer));
    printf("sync over fileptr=%x ReadBuffer=%x\n",fileptr,ReadBuffer[temp_ret]);
    f_lseek(&fmp3,fileptr);

 

到这里就可以开始读取数据帧了。