把该死的mp3歌名找出来II

                        把该死的MP3歌名找出来Ⅱ
                                          -新版ShitMP3 v1.07制作揭密
                                   星集工作室 张麟
起因
 在《程序员CSDN开发高手》第六期,我发表一篇题为《把该死的MP3歌名找出来》(以下称为《把》文)的文章。其实与其说是一篇文章,不如说是一篇VC++例程的讲析,文中讲到了如何通过检索MP3文件的TAG信息最终实现对MP3文件自动重命名。我很高兴许多读者对此文很感兴趣。在过去的两个多月的时间里,我收到了很多读者和ShitMP3用户发来的Email,大家提出了不少的疑问和要求,有的朋友还纠正了代码的不足之处,更有一些热心的朋友还编写了很多代码以补充ShitMP3的功能缺陷。在此,我要对这些读者朋友的关注表示感激,谢谢大家。

 此后的一段时间里,我对MP3的歌名信息和重命名做了更深一步的研究,并结合了很多同行和用户的意见,对MP3文件信息读取与存储技术进行了更深入的学习,我觉得其中还有很多的东西可以让大家再进一步的了解。其实《把》文中的ShitMP3v1.05版确实已经实现了提取MP3歌名同时再把MP3的名称重命名的功能,只不过我觉得既然大家都那么感兴趣,我们干脆就接着更深一步的了解了解不是更好吗。为此我制作了ShitMP3的一个新的版本v1.00.07。本文我就拿这个新款的ShitMP3软件为例,与大家进一步的讨论。目的并不是想宣传什么,而是我觉得讨论确实是一种互相促进的好事情。而且我觉得这款新的ShitMP3不仅是我自己的作品,也算是大家的作品了。所以,如果你愿意,那么我们再次进入MP3歌名提取的世界。^_^
 
更深的原理

原理1:MP3文件的基本信息-“ID3v1”信息的读取
 在《把》文里我介绍了我们如何从一个MP3歌曲文件里提取简单的歌曲信息,然后再对MP3的文件名的重命名的一套方法。这套方法其实并不是很难,我们只需要从MP3文件信息里找到相应的歌手名,歌曲名就可以了。而接下来我们要研究的是更深一步的解析MP3的歌曲信息,这就需要我们也要更深一步的了解MP3的歌曲信息结构了。

 在《把》文中曾经提到,MP3的基本歌曲信息存在了MP3文件的最后128个字节里,其结构是:
typedef struct      //MP3信息的结构
{
 char   Identify[3];  //TAG三个字母
        //这里可以用来鉴别是不是文件信息内容
 char   Title[30];   //歌曲名,30个字节
 char   Artist[30];   //歌手名,30个字节
 char   Album[30];  //所属唱片,30个字节
 char   Year[4];   //年,4个字节
 char   Comment[28];  //注释,28个字节
 unsigned char reserved;   //保留位,1个字节
 unsigned char reserved2;  //保留位,1个字节
 unsigned char reserved3;  //保留位,1个字节
} MP3INFO;


1 Winamp里看到的MP3的ID3v1信息


 这里有几个地方值得我们更深一步的探讨。如果我们想把一个MP3重命名,那么我们只要提取出Title(歌曲名),Artist(歌手名),最多再加上Album(专辑名)就够了。就如ShitMP3v1.05里面那样把MP3的歌名从类似于“12341234214.mp3”转换成“(歌手名)歌曲名.mp3”。

而就如有些读者问到“Winamp的MP3的全部基本信息是怎么从这个基本结构里知道的?”我们就要把这128字节的信息全部解析出来了。好,我们首先看一下一个MP3文件在Winamp里显示的信息(如图1)。
 我想声明,这128个字节的MP3歌曲信息,有个名字:叫做“ID3v1”。我们姑且别去管他为什么叫做“ID3v1”,就像我们问“为什么这个叫做饺子?”一样,并没有多大意义。我们要知道这个结构到底存储了哪些信息。在《把》文里,我们知道了这个信息分别存储了(如图2):

2 一个MP3文件的ID3v1信息


1-3   TAG
4-33  歌曲名
34-63  歌手名
64-93  专辑名
94-97  年
98-125  备注
126-128  3个保留位

而在Winamp的ID3v1歌曲信息里(如图1),我们看到的是他都包括:
Title(歌曲名)
 Artist(歌手名)
 Album(专辑名)
 Year(年)
 Comment(备注)
 Genre(歌曲风格)注,见本文附表1
 Track#(歌曲在专辑里的顺序,就是我们经常说的“第几首”)

奇怪了,Title,Artist,Album,Year,Comment我们都是可以在那个128个字节里得到,Genre和Track哪里去了呢?其实我们都重视了那128字节信息的前125个信息了,而这两个信息是却放在了最后的126-128字节里。其实,Reserved2就是Track信息,而Reserved3就是Genre信息。他们的存储方式都不是字符,我们提取他们的时候需要注意,他们都是数字。比如,就如我们看到的这首歌的Reserver3是0x0D,那么很显然,他就是13。也就是第13号歌曲风格,Pop流行(查附表1)。

 这时,你也该猜到了,Reserved2和Reserved3都是有意义的,自然Reserved1也是有其意义!我们在《把》文里知道ID3v1信息的Comment(注释)一共占用28个字节。这个说法并不是完全的正确。准确的说应该是正确了一部分。有的时候注释也可以超过这个数字的。ID3v1要求注释最多可以到30个字节。那么有的读者会问“MP3的ID3v1就是得有130个字节的信息了嘛?”不是,当然不是。ID3v1是固定的128个字节,这个你不用担心。其实ID3v1是这样安排的:如果MP3的注释是大于28个字节的,那么就要借用126-127两个字节。所以ID3v1的注释部分可能是28个字节也可能是30个字节。那么,怎么区分到底是28个字节还是30个字节呢?很简单,Reserved1就是管这个的,我们只要看看Reserved1是不是0x00,如果是0x00那么注释就有28个字节。如果不等于0x00,那么就是说注释是30个字节。同时别忘了,由于第127字节存储了Track信息,那么如果注释是30个字节的时候,这首歌的ID3v1里的那个Reserved2的信息自然就不是Track信息了。Track自然就是没有地方存了,所以Reserved2变的没有Track意义了,它只是Comment的一部分了。在你决定制作读取ID3v1的程序的时候,请特别注意一下。

 我们最终知道了Reserved1是ID3v1信息的注释部分到底是28个字节还是30个字节的标志位。Reserved2是音轨信息(Track),而Reserved3则是歌曲风格(Genre)。现在我们重新再写一次结构

(ShitMP3v1.07里的结构):
typedef struct      //MP3信息的结构ShitMP3v1.07
{
 char   Identify[3];  //TAG三个字母
        //这里可以用来鉴别是不是文件信息内容
 char   Title[30];   //歌曲名,30个字节
 char   Artist[30];   //歌手名,30个字节
 char   Album[30];  //所属唱片,30个字节
 char   Year[4];   //年,4个字节
 char   Comment[28];  //注释,28个字节(也可能30个字节啊!)
 unsigned char CommentFlag;  //保留位1,注释长度标志位
 unsigned char Track;   //保留位2,那个“第几首”,是个整数
 unsigned char Genre;   //保留位3,歌曲风格,在0-148之间
} MP3INFO;
 
原理2:MP3文件有没有ID3v1信息的错误理解
 在ShitMP3v1.05版里我确实忽视了一个问题,那就是到底什么能叫做“MP3文件没有ID3v1信息”。检测的方法是先提取指定的MP3文件的最后128字节信息,然后确定这128个字节的前3个字节是“TAG”。很多朋友都会同意这个方法没有问题的。可是,实际上问题并不是那么简单。


3 最让人讨厌的“假ID3v1”


 Winamp或者其他的MP3播放相关的软件都有MP3信息的写入和读取的功能,然而这些写入ID3v1的软件都会不自觉的当你一打开这个MP3文件就会给它加上这128个字节的信息。也就是说当我们用这种软件打开MP3文件的时候,这些软件就会自动的在这个MP3文件尾端添加了一个128字节的ID3v1结构,而且还是以“TAG”开头!(如图3)。那么很显然,光靠检测那“TAG”三个字节的信息,还是不能完全确定MP3到底有没有ID3v1信息的。我们还要确定这“TAG”后的125字节是不是正确的信息。一般情况下,这类软件产生的ID3v1结构都是由一堆00,或者一堆空格组成的,所以我们要判断一下是不是ID3v1的信息是一堆00或者一堆空格。如果是,那么MP3文件虽然有这“TAG”三个字母,却仍然不是一个合法的ID3v1信息。MP3文件仍然应该认为没有ID3v1信息。我觉得这个东西有必要特别提醒大家注意。

原理3:ID3v2信息的提取
在《把》文中,我们提到了MP3的歌曲信息存储在整个MP3文件的最后128个字节里,我们把这个信息叫做MP3文件的“ID3v1信息”。这个信息结构提取起来非常容易,写入到文件也不是什么难事。但是它的信息安排和可扩展性却非常之差(只能128个字节)。就如你所知,MP3文件还有另外的一个信息结构,这个结构具有更好的可扩展性,而且存储的容量也不受限制(也就是总长度不固定)。这个信息就是ID3v2信息(相对ID3v1而言)。由于ID3v1信息存储在了文件的最后128个字节里,那么ID3v2就不得不放弃选择存储在文件的末尾了,于是它被存储在了文件的起始位置。

ID3v2信息的存储和读取远远要比ID3v1信息复杂的多。这是因为ID3v2信息不再固定,而且由于这段信息存储在了文件的首端,所以重新写入的时候也远比ID3v1麻烦的多。

我用尽可能清楚而且简练的话,给大家讲一下ID3v2信息的读取方法。ID3v2到现在一共有4个版本,不过比较流行的MP3播放软件一般只支持第3版,即ID3v2.3。我们要读取的就是ID3v2.3信息。ID3v2信息包括两个部分,一个部分是标头信息,另一个部分是标体信息。其中标头信息占固定的十个字节,它的结构如下:

struct ID3v2Header
{
 char identifier[3]; //ID3v2标识位,应该是“ID3”三个字母为对
 struct
    {
  byte major; //主版本,一般就如我们上面所说,这个信息是3
  byte revision; //副版本,一般就是0
 }version;   //版本信息结构
 byte flags;   //标志位,我们一般很少使用,你就认为他是0好了
 byte size[4];  //每byte数值不能大于0x80,即该size值的每个bit 7均为0
     //这里存储了整个ID3v2标体信息的长度。
};

这里得说明一下,我们提取这十个字节的信息,目的只有两个:一个是提取identifier看看是不是“ID3”;另一个是提取size,得到ID3v2的长度,便于我们提取后面的标体信息。这四个字节的size存储上并不是我们用一个int类型的变量直接接收就可以的。Size的每个字节的大小是0-128的7位二进制数,并不是0-256的8位二进制数,每个字节的最高位是不使用的。所以我们得特别计算一下,计算方法如下:

int ID3size;
  ID3size =   (Size[0]&0x7F)*0x200000
      +(Size[1]&0x7F)*0x400
      +(Size[2]&0x7F)*0x80
      +(Size[3]&0x7F);
通过解析这段标头信息我们可以知道一个MP3文件是不是有ID3v2信息,如果有我们就知道了ID3v2的数据体的总长度。
接下来我们要解析ID3v2的数据体,别担心,虽然复杂,但也没你想象的那么的痛苦。ID3v2的数据体又分为很多相同的数据结构。每一个相同的数据结构又分为一个标头,一个数据体。我说的可能有些复杂,实际上也没那么复杂。首先我们还是看看一个数据结构的标头:

struct ID3v2FrameHeader
{
 char Frameid[4]; //结构标志位,比如,如果是歌曲信息,那么这四个字节就是TIT2
 byte size[4];  //这个结构体的数据体的长度
 byte flags[2];  //标志位,也是很少用到,由于篇幅,不作太多的解释了
};
其中要说明的是这个FrameID,在ID3v1里我们是根据每一个信息所占用的固定的字节数和位置来判断他是哪个信息的。而ID3v2为了提供更好的可扩展性,把这些信息变得“动态”化了,因为长度并不是预先设定好的,而是在size[4]里存储的。这样长度就可以不再固定了。我觉得在我们自己定义文件的时候ID3v2和ID3v1也是值得我们考虑的一个方面。如果结构很小而且存储的量也不大,我们可以采用ID3v1的信息存储方式。如果存储的信息不固定,而且要求有很好的可扩展性,那么ID3v2当然成了首选。实际上,现在很多格式的文件的存储方式都是ID3v2的存储方式非常接近的。JPEG可以说是这种存储方式的典范了。

好了,回到正题,先介绍一下FrameId已经定义好的几种标志位(ShitMP31.07里使用的):

标志位 标志含义
TIT2 歌曲名(Title),相当于ID3v1的Title[30]
TPE1 歌手名(Artist),相当于ID3v1的Artist[30]
TALB 专辑名(Album),相当于ID3v1的Album[30]
TRCK 音轨(Track),也就是专辑里的第几首歌,相当于ID3v1的Track(Reserved2)
TYER 年(Year),相当于ID3v1的Year[4]
TCON 乐曲风格(Genre),相当于ID3v1的Genre(Reserved3),(见附表1)
COMM 注释(Comment),相当于ID3v1的Comment[28]
TENC 编码方式(Encode By)
TCOP 版权(Copyright)
WXXX URL链接(URL)
除了上述的标志位外,还有很多其他的标志位,只不过不是很常用而已。你可以到www.ID3.org去了解一下。所以,我们可以通过解析出这个数据结构的标头来了解,这段数据结构体到底存储的是哪一类信息,然后按照得到的size大小,提取出相应的信息。直到全部读完。

再有就是size部分的存储,不再是总标头那样的每个字节只取后7位了,它是按照正常的8位存储的。得到一个数据体的大小的方法如下:

int FSize;
  FSize =  Size[0]*0x100000000
     +Size[1]*0x10000
     +Size[2]*0x100
     +Size[3];

为了方便大家的理解和领悟,我们还是拿一个MP3文件举例子吧。如图3,我打开了一个有ID3v2信息的MP3文件,是我喜欢听的华健大叔的秋的别馆。
首先我们先看一下ID3v2的总标志头:

1-3字节:ID3
第4字节:3
第5字节:0
第6字节:0
7-10字节:00 00 01 50

从这十个字节我们可以知道了,这个MP3文件是有ID3v2信息的(因为前三字节是ID3),第四字节说明是ID3v2的第三版本。最后四个字节是ID3v2数据体的长度,我们通过上面提到的计算方式得到ID3v2数据体的长度是:

(1&0x7F)*0x80+(50&0x7F) = 208

4 一首MP3歌曲的ID3v2信息


如图4,高亮部分的就是这个ID3v2的标头和它后面的208字节的信息。

标头后面就是数据体了,我们提取数据体的前十个字节,我们知道了这个数据结构存储的FrameID是TIT2,查上面的表,说明这个数据结构存储的是歌曲名信息。大小是00 00 00 14,换成十进制就是20。也就是歌曲名是这个子标头后的20个字节的信息。也就是:“Love Hotel 秋的别馆”。接下来的一个数据结构的FrameID是TPE1,说明是歌手名,而大小是00 00 00 07,说明这个数据体有7个字节,也就是:“周华健”。依次类推。这里需要大家知道的是一个汉字占用两个字节。

还有特别要提醒大家的是,ID3v2的注释信息(FrameID是COMM)的数据体的前四个字节,并不是注释内容,而是注释使用的自然语言,这个例子里我们看到是:”eng/0”,我们要跳过这四个字节的信息进行解析。此外ID3v2的歌曲类型Genre(FrameID是TCON)的存储也不太一样的。由于很多MP3播放器的写入方式并不是非常一致,而在Genre写入的也不一致。比如,这首歌的ID3v2的Genre是Classic Rock,其实有的还会写入成:(1),或者1,还有(1)Classic Rock,所以格式五花八门,我们要在解析的时候注意一下。还有,值得一提的是winamp在保存和读取帧内容的时候会在内容前面加个'/0',并把这个字节计算在帧内容的大小中。所以前面提到的歌手名“周华健”本身应该6个字节,可是却占了7个字节。

原理4:ID3v2信息的写入
 这是更麻烦的一个步骤,不过如果你已经有了ID3v2读取的经验,讲起来也不算太费劲吧。首先我们还是用先用一个结构得到要写入的信息,当然ID3v2的信息本身就是不固定的,不过我们姑且就写入歌曲名(Title),歌手名(Artist),专辑名(Album),歌曲类型(Genre),出品年份(Year),序号(Track),注释(Comment),版权(Copyright),编码方式(Encode by)和URL网址(URL)10种信息。
 
//ID3v2信息结构
typedef struct
{
 CString  Title;   //歌名
 CString  Artist;  //艺术家
 CString  Album;  //专辑名
 CString  Year;   //年
 CString  Comment;  //注释
 CString  Genre;  //属于哪一种风格的歌曲,见g_arrMP3Genre数组
 CString  CopyRight; //版权
 CString  EncodeBy;  //编码方式
 CString  URL;   //URL网址
 BYTE   Track;  //是唱片里的第几首歌
}MP3ID3v2Info;

我们得到这些信息的时候,要按照刚才解析的方法进行逆运算。倒着把每一个数据体的标头写出来,然后再把总标头写出来,最后再把整个信息从头倒尾输入给文件就可以了。

 由于我们要把信息写到文件头,所以写入的方法就也比ID3v1写到文件最后难很多。可能是我所知道的方法有限吧,到现在我还没有找到什么可以把一段数据插入到某个文件的头部的方法。唯一的方法就是重写文件。

 一般情况下,把一段数据插入到一个文件的方法是,首先把插入点以前的数据,和插入点以后的文件写到内存中,然后重新建立一个文件,把插入点前的数据从内存存储到文件里,再写入要插入的信息,最后再把插入点文件写进去就可以了。其实这样也并不是我们想象的会很慢,只不过制作起来有些麻烦而已。顺便说一句,严禁把插入点前后的两个部分写到临时文件里,那可是太耽误时间了,一个MP3文件可得有3-4MB啊!写完再删,黄花菜都凉了。

 由于ID3v2是写在MP3文件的首部的,所以我们只要把插入点以后的MP3文件数据写到内存里就可以了。

更深一步制作:

任务1:让ShitMP3能够显示全部的ID3v1信息

5 ShitMP3v1.07里显示ID3v1信息

如图5就如你看到,ShitMP3可以显示一个MP3文件的ID3v1全部信息,这个并不是很难,首先,我们要从MP3文件中读出ID3v1的结构:

//错误定义
#define NOFILE   -1;  //没有这个文件
#define FILEUNOPEN   -2;  //文件无法打开
#define NOTAG   -3;  //没有MP3的标志位
#define ALLBLANK   -4;  //标志信息全都是空的
#define NORENAME  -5;  //无法更换文件名
BOOL StarGetMP3Info(CString filename, MP3INFO *mp3)
{
 CFileFind find;
 if(!find.FindFile(filename))
  return NOFILE;//路径不存在

 CFile file;
 //文件无法打开
 if(!file.Open(filename,CFile::modeRead))
  return FILEUNOPEN;
 
 long seekPos = 128;
 file.Seek(-seekPos,CFile::end);
 
 BYTE pbuf[128];
 memset(pbuf,0,sizeof(pbuf));

 file.Read(pbuf,128);
 
 //获得tag,如果不是tag,那么就返回

 if(!((pbuf[0] == 'T'|| pbuf[0] == 't')
  &&(pbuf[1] == 'A'|| pbuf[1] == 'a')
  &&(pbuf[2] == 'G'|| pbuf[0] == 'g')))
 return NOTAG;
 memset(mp3,0,sizeof(*mp3));
 memcpy(mp3,pbuf,sizeof(*mp3));
 return TRUE;
}

这样MP3的ID3v1就全部存储到了MP3INFO结构的变量里了。我们只要把这些数据在显示到视图上就行了。

任务2:让ShitMP3把ID3v1信息存储到MP3文件里

 用户要是希望修改MP3文件的信息,我们自然要把这个信息重新在MP3文件里更新。当然,如果这个MP3文件根本还没有ID3v1信息,那么我们就要先生成一个128字节的ID3v1结构放到文件最后,如果更新的话,我们就是把文件的最后128字节重新写一遍就可以了。

BOOL CMP3::RefreshMP3Info(CString filepath,MP3INFO *mp3)
{
 BOOL re = FALSE;
 CFileFind find;
 if(!find.FindFile(filepath))
  return NOFILE;  //路径不存在
 CFile file;
 //文件无法打开
 if(!file.Open(filepath,CFile::modeReadWrite))
  return FILEUNOPEN;
 long seekPos = 128;
 file.Seek(-seekPos,CFile::end);
 BYTE pbuf[128];
 memset(pbuf,0,sizeof(pbuf));
 file.Read(pbuf,128);
 //如果文件没有ID3v1信息
 if(!((pbuf[0] == 'T'|| pbuf[0] == 't')
  &&(pbuf[1] == 'A'|| pbuf[1] == 'a')
  &&(pbuf[2] == 'G'|| pbuf[0] == 'g')))
 {
  file.SeekToEnd();   //把文件指针置到文件最后的位置
 }else
 {
  file.Seek(-seekPos,CFile::end);//把文件指针置到文件倒数第128位
 }
 file.Write(&mp3F,sizeof(mp3F)); //把ID3v1写到文件里
 return TRUE;
}

 

6 ShitMP3里面显示ID3v2信息

任务3:让ShitMP3显示MP3文件的ID3v2信息

刚才在原理里我们讲到了MP3文件的ID3v2信息,读取这种信息比较麻烦。我们需要有一个指针,首先获得ID3v2的总标题信息,从那里获得整个ID3v2信息体的长度和ID3v2的标志是否存在,如果存在ID3v2标志,我们就按照数据结构的先后顺序一个一个的读出来,直到这个指针指到MP3文件ID3v2信息体长度值的位置为止。期间,每得到一个数据结构,就提起那个数据结构的数据标头,得到这个结构的长度,然后再读取出这个结构里存储的内容。
首先我们先定义一下FrameID表:
#define ID3V2_FRAMEID_TITLE  "TIT2" //歌曲名
#define ID3V2_FRAMEID_ARTIST  "TPE1" //歌手名
#define ID3V2_FRAMEID_ALBUM  "TALB" //唱片名
#define ID3V2_FRAMEID_TRACK  "TRCK" //唱片序号
#define ID3V2_FRAMEID_YEAR  "TYER" //年
#define ID3V2_FRAMEID_COMMENT "COMM" //注释
#define ID3V2_FRAMEID_GENRE  "TCON" //歌曲风格
#define ID3V2_FRAMEID_ENCODEBY "TENC" //编码方式
#define ID3V2_FRAMEID_COPYRIGHT "TCOP" //版权
#define ID3V2_FRAMEID_URL   "WXXX" //URL网址

由于ID3v2的是以前三个字节是不是“ID3”组成的,所以我们有必要还要在读取之前检测一下,当前的MP3文件是不是满足这个条件。
BOOL HasID3v2Tag(CString Path) //参数位文件路径
{
 CFile file;
 if(!file.Open(Path,CFile::modeRead))
  return FALSE;  //文件无法打开
BYTE id3[3];
memset(id3,0,sizeof(id3));
file.Read(id3,3);
if((id3[0] == 'I') && (id3[1] == 'D') && (id3[2] == '3'))
    {
  return TRUE;
 }
    else
    {
  return FALSE;
 }
 file.Close();
}
检测到指定的MP3文件确实有ID3v2信息之后,我们要做的就是从前十个字节里获得我们要的标头信息,而这十个字节的标头信息,对于我们最重要的除了,检测是不是存在“ID3”标志位,剩下的就是得到整个ID3v2信息体的长度。

在上面的原理里面我们已经知道怎么做了,这里我就也不再重复了,只要读取有ID3v2信息的MP3文件的第7-10字节就可以获得。

得到这个数据体的长度之后,接下来我们就要遍历所有的数据结构,从而解析出ID3v2的各项数据结构的内容。首先我们需要定义一个指针,让他只想ID3v2数据体部分的起始位置(也就是有ID3v2信息的MP3文件的第11字节)。然后首先获得第一个数据结构的标头,从数据结构的标头里得到相应的数据FrameID,看看到底是哪一类信息,然后再得到这个数据结构的数据内容的长度,有了这两个信息,我们就可以得到一个数据结构的内容了,把这个辛辛苦苦得来得数据保存在一个变量里我们就完成一个数据结构得解析任务。

void GetFrameHeader(CFile *pFile)

 if(!HasID3v2Tag())
  return;
 DWORD dwPos=10;   //这个就是我们上面提到的所谓的指针
 char cNull=0;

 pFile->Seek(10,CFile::begin); //让文件指针放在第11个字节的位置
 for(;;)
 {
  if(dwPos>=m_nSize)  //假设m_nSize是我们上面得到的数据体的总长度
   break;

  //数据结构的标头结构,在上面的原理里我们曾经提到
  ID3v2FrameHeader tFrmHeader;  
  pFile->Read((void*)&tFrmHeader,10);

  if(strcmp(tFrmHeader.FrameID,&cNull)!=0)
         {
//假设GetFrmHeaderSize()函数可以得到一个数据结构的数据体长度,这个
//算法我们同样在前面的原理里面提到
dwPos=dwPos+GetFrmHeaderSize(tFrmHeader)+10+1;
   
   //获取常规FrameID
   //Title标题
   if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_TITLE,4)==0)
             {
    ……
   }
   //Artist 艺术家
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_ARTIST,4)==0)
             {
    ……
   }
   //Album 唱片集
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_ALBUM,4)==0)
             {
    ……
   }
   
   //备注是以:eng.开始的,所以应该向后搓4个字节
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_COMMENT,4)==0)
   {
    ……
   }
   //年
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_YEAR,4)==0)
   {
    ……
   }
   //Track
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_TRACK,4)==0)
   {
    ……
   }
   //Genre歌曲风格
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_GENRE,4)==0)
   {
……
   }
   //Encode编码方式
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_ENCODEBY,4)==0)
   {
    ……
   }
   //copyright版权
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_COPYRIGHT,4)==0)
   {
    ……
   }
   //URL网址
   else if(strncmp((char *)frmheader.fh_id,ID3V2_FRAMEID_URL,4)==0)
   {
    ……
   }
   //返回文件下一处欲读取之处
   pFile->Seek(dwPos,CFile::begin);
  }
  else{
   break;
  }
 } 
}

通过这样的检索我们就可以得到每一个数据结构的数据体了,这些数据体自然就是我们要的ID3v2信息。上面的函数里每一个“……”都是提取数据体的过程,基本上都是大同小异。我们只要读出数据结构长度,对于解析出来一个数据内容,那就太容易了。重要的是我们要知道对于类似ID3v2信息的提取检索方法,上面的函数不光对于ID3v2这种信息有效,其实对于JPEG的解析,GIF的解析,其实都有一举同功之妙,所以你可以把这种函数好好保存,我想在很多地方我们还是都会用到的。如果你能把他抽象成一个类,那么就再好不过了

 7 写入ID3v2的数据流程图


任务4:让ShitMP3保存MP3文件的ID3v2信息
这个工作显然是最有挑战性的,不光是因为保存ID3v2信息要执行和上面的解析ID3v2一样复杂的逆操作;更让人头疼的是我们必须知道ID3v2并不像ID3v1存在文件的末尾,我们要想写入,只要直接把文件指针放到文件末尾,然后write就可以了。ID3v2是存在文件的文件头,这就给我们带来了麻烦。我查找了各种资料,很遗憾,没有任何一种可行的方法可以直接在一个文件中插入信息(至少在VC这个语言里,如果你有,那么我将十分感激你的赐教)。但是,我们都知道文件有读和写的功能,所以我们可以借助读和写实现插入操作。

 在我们确定写入一个ID3v2给一个MP3文件的时候,我们要先确定是否这个MP3文件存在ID3v2标签信息,如果文件没有ID3v2标签那么我们就直接加上。有的话我们还要重新写入。具体步骤如图6
 如果一个MP3文件没有ID3v2信息,我们要做的就是把新的ID3v2信息放到内存里,然后再把原来的MP3文件的全部数据放到内存里,然后再把这个在内存里的这个新的MP3文件,写给这个指定的文件就可以了。

 生成ID3v2信息的顺序是先生成每一个数据结构(比如歌手名啦,歌曲名啦),接着生成数据体(也就是把这些数据结构存在一起),然后再生成总的标头,最后再把这些整理好的信息放到内存里。
 生成数据结构方面,我仅拿生成歌曲名为例,其他的大同小异,方法如下:

 int tmplength = 0;
 //生成数据结构歌曲名信息(TITLE)的标头
 ID3v2FrameHeader TitleHeader;//原理里提到的那个数据结构标头的结构
 memset(&TitleHeader,0,sizeof(TitleHeader));
 CString TitleString = "";
 //假设用户输入的ID3v2歌曲名放在一个叫做m_ctrlEditSong的文本框里
 m_ctrlEditSong.GetWindowText(TitleString);//得到歌曲名的标体
 //原理里提到Winamp都是在内容的第一个字节多加一个/0,所以自然长度也得多1
 tmplength = TitleString.GetLength()+1;
 //把这个长度,写入真正的结构里。
 TitleHeader.size[3] = (BYTE)tmplength;
 TitleHeader.size[2] = (tmplength-TitleHeader.size[3])/256;
TitleHeader.size[1] =
(tmplength-TitleHeader.ifh_size[3]-TitleHeader.size[2]*256)/256/256;
 TitleHeader.size[0] =
(tmplength-TitleHeader.ifh_size[3]-TitleHeader.size[2]*256-TitleHeader.ifh_size[1]*256*256)/256/256/256;
  //写入FrameID,在提取ID3v2那一节里我们提到了FrameID那个定义表
 memcpy(TitleHeader.FrameID,ID3V2_FRAMEID_TITLE,4);
 //存储上这个数据结构的,标头和标体的总长度,因为我们最后得到总标头的时候使用
int TotalTitleSize = tmplength+10;

 好了我们通过上面的算法,我们最终得到了TitleString(歌曲名的数据体)和TitleHeader(歌曲名的数据结构标头),还有得到了一个TotalTitleSize用来方便计算出来ID3v2的总长度。用同样的方法,我们就可以依次得到所有的ID3v2的数据结构的数据体和数据结构标头。

 下面我们要生成那个10字节的总标头,刚才我们介绍了TotalTitleSize变量里存储了相应的歌曲名数据结构的总长度,我们把这些数据结构的总长度加在一起就得到了我们要的总标头的总长度。这里还是提醒,别忘了,总标头的size是每位最大128,不是256。(详细去看上面的原理)。

 //生成ID3v2总标头
 ID3v2Header Header;    //前面原理里提到的这个总标头结构
 memset(&Header,0,sizeof(Header));
 memcpy(Header. identifier,"ID3",3);  //写入ID3三个字节的标志位
 Header. version.s_major = 3;   //写入版本
 Header. version.s_revision = 0;   //副版本
 Header. flags = 0;     //标志位
 int TotalSize = TotalTitleSize    //刚才的歌曲名数据结构长度
    +TotalArtistSize   //假设:歌手名数据结构长度
    +TotalAlbumSize  //假设:专辑名数据结构长度
    +TotalCommentSize  //假设:注释数据结构长度
    +TotalYearSize   //假设:年数据结构长度
    +TotalGenreSize  //假设:歌曲风格数据结构长度
    +TotalTrackSize   //假设:音轨数据结构长度
    +TotalEncodeBySize  //假设:编码方式数据结构长度
    +TotalURLSize   //假设:URL网址数据结构长度
    +TotalCopyRightSize;  //假设:版权数据结构长度
 //写入总长度,记住是每位最大128
 Header.size[0] = TotalSize/128/128/128;
 Header. size[1] = (TotalSize-Header. size[0]*128*128*128)/128/128;
 Header. size[2] = (TotalSize-Header. size[0]*128*128-Header. size[1]*128*128)/128;
 Header. size[3] =
(TotalSize-Header. size[0]*128*128-Header.size[1]*128*128-Header.size[2]*128);
好了,这些信息都存在了指定的结构或者变量里,接着我们要做的是把这些数据放到一个内存文件里,在VC++里有一个特殊的文件类,他叫做“CMemFile”。这个类可以帮助我们生成一个内存里的文件(说白了,就是在内存里划出来一个空间存储信息)。
 //假设:FileLength是原先文件的长度
//假设:ID3Size是原先MP3文件中的ID3v2信息长度,当然如果没有ID3v2,那么就是0
//TotalSize是新的ID3v2的数据体长度,再加上10,就是新的ID3v2信息的总长度
//所以最后得到的MemFileLength就是新文件的长度了
 DWORD MemFileLength = FileLength-ID3Size+TotalSize+10;
 //新建内存文件
CMemFile memfile;
 BYTE* buffer=(BYTE*) malloc(MemFileLength);
 memfile.Attach(buffer,MemFileLength,1024);
 memfile.SetLength(MemFileLength);
 //写入全部信息
 //写入ID3v2总头
 //Header是上面提到的总标头信息
 memfile.Write(&Header,sizeof(Header));
 BYTE tmp = 0;
 //写入歌曲名
 memfile.Write(&TitleHeader,sizeof(TitleHeader));
 memfile.Write(&tmp,1);
 memfile.Write(TitleString,TitleString.GetLength());
 //写入歌手名
  ……
 //写入专辑名
  ……
 //写入注释
  ……
 //写入年
  ……
 //写入Track
  ……
 //写入Genre
  ……
 //写入CopyRight
  ……
 //写入Encode
  ……
 //写入URL
  ……
 //写入MP3主体信息
 file.Seek(ID3Size,CFile::begin);
 BYTE *tempbuffer = new BYTE[FileLength-ID3Size];
 file.ReadHuge(tempbuffer,FileLength-ID3Size);
 file.Close();
 memfile.WriteHuge(tempbuffer,FileLength-ID3Size);

好了,新的文件已经被写入了内存中,由于写入各种数据结构完全一样,这里我就只拿写入歌曲名作为例子了。“memfile.Write(&tmp,1);”是往文件里写入一个“/0”,我们在原理里提到的winamp在保存和读取帧内容的时候会在内容前面加个'/0',并把这个字节计算在帧内容的大小中。另外我们不要忘了注释的写入上,有些不大一样,别忘加上“eng/0”(原理里面也提到了)。此外,细心的读者可能注意到,写文件的时候我使用了WriteHuge而没有使用Write,这是因为在VC++里,写入大于64K的数据的时候,要使用WriteHuge函数,因为再用Write就非常的慢了。一个MP3的文件体,怎么说也得有3-4MB吧,所以自然要选择WriteHuge。当然ReadHuge也是一个道理。
最后一步工作就是把新的,放在内存里的信息写给文件就可以了。别忘了清理内存。

 //重写文件
 //假设m_sFilePath是文件的路径
 if(file.Open(m_sFilePath,CFile::modeCreate|CFile::modeWrite))
 {
  file.WriteHuge(buffer,MemFileLength); //上面说到的,不要使用Write
 }
 file.Close();  //关闭文件
 memfile.Detach(); //释放内存

后记
 我还是要再次感谢很多热心的读者和程序员同行给我提出的建议,新的代码里有一部分是他们的功劳。新版本的ShitMP3 v1.00.07您可以到http://www.monocers.com/jon/shitmp3/shitmp3107.rar去下载,一些相关的下载网站也有107版的下载,请大家多多提出宝贵意见。当然,我在征求了很多代码提供者的意见之后,也开放了新版本的ShitMP3的内核类源代码,支持了ID3v2提取和写入,以及ID3v1的相关操作,你可以到http://www.monocers.com/jon/shitmp3/shitClass2.rar去下载。上一版源码提供中,有些读者发来Email希望得到一些相关的源码,为此,我在这个ShitClass2包里,额外写了一个例子程序,是用VC++6.0写的,希望对你更好的使用这个类有所帮助。
 我觉得ID3v2的提取和存储,是一个非常能锻炼你编程技术的东西。而且学会了,能对我们解析各种类型的流式文件,非常有帮助。当初大学毕业的时候我曾经选择了《JPEG的压缩技术》为自己的毕业论文。当中JPEG的提取与储存虽然远比ID3v2难上很多,但麻雀虽小五脏据全。如果你想了解很多文件的存储和提取技术,ID3v2绝对是值得你很好的学习的一种技术。我建议如果你有空,请你自己独立的写一写,看看你能不能用自己的代码提取出来ID3v2信息,如果你能做到了这些,那么祝贺你。你的流式文件提取与存储技术已经到了一定火候了。希望大家因为我的一段小小的代码对MP3文件信息的读取和存储有所了解,更希望这些代码能够给你的编程学习带来方便。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值