1998年,ID3V2作为新的标准诞生了,尽管其沿用了“ID3”的名称,但是ID3V2和ID3V1并没有太多联系。ID3V2定义在MP3文件的头部,这与ID3V1不同。ID3V2是变长的,这一特性使ID3V2具有良好的扩展性,甚至个人也可以定义ID3V2中的帧,只要符合ID3V2的布局即可。
ID3V2的结构图如图9-8所示,其中深色标识的结构是可选的。在ID3V2中,比特顺序采用Big endian方式排列,也就是高字节存储在高位。ID3V2的结构相对于ID3V1复杂很多,这里重点介绍ID3V2头和ID3V2帧。
图9-8 ID3V2的结构图
1.ID3V2头
ID3V2头(Header)的长度是固定的,共10个字节,其布局如图9-9所示。
前3个字节总是“ID3”,可以通过检查这个文件标识来判断是否是ID3V2头。随后2个字节是ID3V2的版本,其中第4个字节代表ID3V2的主版本号,第5个字节代表ID3V2的修订版本号。目前ID3V2的2.3和2.4版本应用最广泛。随后的一个字节是标志位,目前此字节的前4位在使用,其他位为0,标志位的第2位标识了ID3V2头后面是否有扩展头,标志位的第4位标识了ID3V2最后是否含有Footer。最后的4个字节标识了ID3V2的大小,其中包括10个字节的ID3V2头。由于每个字节的第1位永远是0,因此只有28个字节用来表示大小。计算大小时可以采用下面的代码:
int tagSize = (header[9] & 0xff) + ((header[8] & 0xff) << 7)
+ ((header[7] & 0xff) << 14) + ((header[6] & 0xff) << 21);
图9-9 ID3V2头的布局
2.扩展头
扩展头(Extended Header)包含了更多的数据信息,这些数据是对ID3V2头的补充,但是并非解析MP3文件的关键数据。
3.ID3V2 帧
每个ID3V2标签含有一个或者多个ID3V2帧,每个帧由ID3V2帧头和帧体构成。ID3V2帧头由4个字节的帧ID、4个字节的大小标识和2个字节的标志位组成,共计10个字节。帧头的布局如图9-10所示。其中帧ID由4个字符组成,字符可以是0~9和A~Z,例如TIT2、TALB等。紧随其后是4个字节的尺寸标识,4个字节的每个比特都可以使用,共计32位用来表示帧的大小。需要注意的是,这个大小表示的是帧体的大小,不包括帧头的10个字节,因此整个尺寸应该是帧体的大小加上10个字节。标签帧并没有固定的顺序要求,TIT2可以出现在TALB前面,也可以出现在TALB的后面。
图9-10 ID3V2帧头的布局
ID3V2的帧体由字节数组构成,其内容一般是与帧ID对应的。例如,TIT2帧体内存储了歌曲的标题,TALB帧体内存储了歌曲的专辑信息。帧体的第1个字节标识了字符的编码方式,目前有4种编码方式可用:
0000 0000代表字符使用ISO-8859-1编码方式。
0000 0001代表字符使用UTF-16编码方式。
0000 0002代表字符使用 UTF-16BE编码方式。
0000 0003代表字符使用UTF-8编码方式。
在读取帧体内容时,应该按照上面的编码对应表首先确定编码方式,然后再生成相关的字符串。对于TIT2和TALB等帧ID来说,读取其内容比较简单。对于USLT(对应歌曲的歌词信息)等结构较复杂的帧,需要仔细研究其格式才能将内容从帧体中读取出来。
4.填充
在ID3V2帧后面可以存放填充(Padding)位,填充位的值只能是0。填充位使得ID3V2帧的大小比ID3V2计算得到的大小要小一些。也就是说,留下了一些空白的空间,这些空间可以用来增加一些额外的帧信息。由于增加的信息写在一些空白的空间内,因此无须重写整个文件,这也就是填充存在的重要意义。
5.ID3V2尾 www.2cto.com
ID3V2 尾(Footer)是可选的,有时候可能需要从MP3文件的尾部向前搜索ID3V2的位置,这时候ID3V2的存在就可以大大地加快搜索的速度。ID3V2 尾和ID3V2 头的内容是一致的,只是文件标识部分由“ID3”改成了“3DI”。
ID3V2的结构相对要复杂一些,在设计ID3V2类时,主要考虑了ID3V2的大小和ID3V2帧。ID3V2的大小可以帮助我们快速定位到MP3帧的起始位置,ID3V2帧内存储了MP3文件的元数据,包括歌曲名称、歌手和专辑等。ID3V2类定义了一个HashMap类型的成员变量,用来存储ID3V2帧数据,以ID3V2帧ID为键,以帧的内容为值。
ID3V2类的源代码如下所示,可以用MP3文件测试ID3V2的解析结果。
public class ID3V2 {
private File file;
private int tagSize = -1;
//存储ID3V2的帧,比如TALB等
private Map<String, byte[]> tags = new HashMap<String, byte[]>();
public static void main(String[] args) {
File f = new File("f:/media/mp3/other/huozhe.mp3");
ID3V2 id3v2 = new ID3V2(f);
try {
id3v2.initialize();
} catch (MP3Exception e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(id3v2.tit2());
System.out.println(id3v2.tpe1());
System.out.println(id3v2.talb());
}
public ID3V2(File file) {
this.file = file;
}
public void initialize() throws MP3Exception, IOException {
if (file == null)
throw new NullPointerException("MP3 file is not found");
FileInputStream is = new FileInputStream(file);
byte[] header = new byte[10];
is.read(header);
//判断是否是合法的ID3V2头
if (header[0] != 'I' || header[1] != 'D' || header[2] != '3') {
throw new MP3Exception("not invalid mp3 ID3 tag");
}
//计算ID3V2的帧大小
tagSize = (header[9] & 0xff) + ((header[8] & 0xff) << 7)
+ ((header[7] & 0xff) << 14) + ((header[6] & 0xff) << 21);
int pos = 10;
while (pos < tagSize) {
byte[] tag = new byte[10];
//读取ID3V2的帧头,如果tag[0]=0,则跳出循环,结束解析ID3V2
is.read(tag);
if (tag[0] == 0) {
break;
}
String tagName = new StringBuffer().append((char) tag[0]).append(
(char) tag[1]).append((char) tag[2]).append((char) tag[3])
.toString();
//计算ID3V2帧的大小,不包括前面的帧头大小
int length = ((tag[4] & 0xff) << 24) + ((tag[5] & 0xff) << 16)
+ ((tag[6] & 0xff) << 8) + tag[7];
byte[] data = new byte[length];
is.read(data);
//将帧头和帧体存储在HashMap中
tags.put(tagName, data);
pos = pos + length + 10;
}
is.close();
}
public int getTagSize() {
return tagSize;
}
public String tit2() {
return getTagText("TIT2");
}
public String talb() {
return getTagText("TALB");
}
public String tpe1() {
return getTagText("TPE1");
}
private String getTagText(String tag) {
byte[] data = (byte[]) tags.get(tag);
//查询帧体的编码方式
String encoding = encoding(data[0]);
try {
return new String(data, 1, data.length - 1, encoding);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
private String encoding(byte data) {
String encoding = null;
switch (data) {
case 0:
encoding = "ISO-8859-1";
break;
case 1:
encoding = "UTF-16";
break;
case 2:
encoding = "UTF-16BE";
break;
case 3:
encoding = "UTF-8";
break;
default:
encoding = "ISO-8859-1";
}
return encoding;
}
}