使用电脑、手机登方式播放音乐时,经常会遇到音乐信息乱码的场景,涉及到的乱码信息包括歌曲名、专辑名、艺术家、流派等。
一. 基础知识
1. 常见编码格式
GBK
GBK对汉字采用双字节编码,对英文、数字等采用单字节编码
GBK 的中文编码范围为8140-FEFE(不包括XXFF),即:- 高位:0x81-0xFE
- 低位:0x40-0x7E、0x80-0xFE
UTF-8
UTF-8编码采用3个字节的头,这3个字节分别为:
0xEF(239) 0xBB(187) 0xBF(191)
UTF-8采用不定长编码,英文为单字节编码,中文为3字节编码
UNICODE转UTF-8规则如下:- Unicode的编码范围为0000-007F, 对应的UTF8为0XXX XXXX
- Unicode的编码范围为0080-07FF, 对应的UTF8为110XXXXX 10XXXXXX
- Unicode的编码范围为0800-FFFF, 对应的UTF8为1110XXXX 10XXXXXX 10XXXXXX
UTF-16 Big endian
UTF-16 big endian, 采用双字节或四字节编码,头部为两个字节,分别为:
- 0xFE(254)
- 0xFF(255)
UTF-16 Little endian
UTF-16 little endian, 采用双字节或四字节编码,头部为两个字节,分别为:
- 0xFF(255)
- 0xFE(254)
UTF-32 Big endian
UTF-32 big endian, 采用四字节编码,头部为四个字节,分别为:
- 0xFF(255)
- 0xFE(254)
- 0x00
- 0x00
UTF-32 Little endian
UTF-32 litter endian, 采用四字节编码,头部为四个字节,分别为:
- 0x00,
- 0x00,
- 0xFE(254),
- 0xFF(255)
Big5
big5采用双字节编码,头部有两个字节,分别为
- 高位:0xA1-0xF9
- 低位:0x40-0x7E、0xA1-0xFE
2. 音乐常见tag
- ID3v1
v1版的ID3在mp3文件的末尾128字节,以TAG三个字符开头,后面跟上歌曲信息
- ID3v2
ID3V2位于文件头部,并且大小是动态的。标签内容以数据帧的形式存在,这样可以克服ID3V1的缺点。 但是ID3V2的缺点是定义比较复杂,处理起来的效率比较低。
ID3V2标签有多个版本:id3v2.2,id3v2.3.0,最新的是id3v2.4.0。这里主要以应用最广的ID3V2.3.0为例来说明ID3V2标签。
每个ID3V2.3的标签都一个标签头和若干个标签帧或一个扩展标签头组成。关于曲目的信息如标题、作者等都存放在不同的标签帧中,扩展标签头和标签帧并不是必要的,但每个标签至少要有一个标签帧。标签头和标签帧一起顺序存放在MP3文件的首部。
3. java String中的字符表示形式
Java中的String内部是由char 实现的。由于String中可能用于显示各种各样的语言、符号等,并且各个语言采用的编码方式可能不同,因此,String内部采用Unicode编码来存储char[]。
二. 乱码原因
1. ID3V2.4中的乱码
2. ID3v1中的乱码
三. 解决方案
1. 乱码校正核心逻辑
st=>start: 读取歌曲信息
e1=>end: 显示正常信息
e2=>end: 显示乱码信息
cond1=>condition: 是否有Tag?
cond2=>condition: ID3V2 or ID3v1?
oper1=>operation: 读取3v2原始字节信息
oper2=>operation: 读取3v1原始字节信息
oper3=>operation: 编码格式识别
cond3=>condition: 是否为已知格式?
oper4=>operation: 根据真实格式解析信息
st->cond1
cond1(yes)->cond2
cond2(yes)->oper1(right)->oper3->cond3
cond2(no)->oper2->oper3->cond3
cond3(yes)->oper4->e1
cond3(no)->e2
2. 整体逻辑
上图这是最基本的校正流程,如果乱码发生了播放器中,就要考虑到整体的流程。
需要考虑以下几个问题:
- 校正歌曲信息是耗时操作,采用何种方式进行校正乱码
- 校正后的信息保存在哪?
- 在哪些场景需要执行乱码校正
第一个问题:校正歌曲信息是耗时操作,采用何种方式进行校正乱码?
可新建一个校正服务(比如CheckService),在CheckService中启动线程执行乱码校正,CheckService的设计可参考Android原生的MediaScanService的架构。CheckService采用bindService的方式启动,并且将校正完成后的回调接口以IBinder格式的参数传入CheckService中,这样校正完毕后就可通知客户端乱码校正已完成。
第二个问题:校正后的信息保存在哪?
考虑到要进行校正的歌曲数量可能比较大,并且可能要将校正后的信息开放给别的应用使用,可将校正后的信息存放到数据库中,并自定义ContentProvider,将保存校正后的歌曲信息的数据库以URI的方式发布出去,便于其他的应用或组件调用。
第三个问题:在哪些场景需要执行乱码校正?
- 一般来说,播放器在第一次启动后需要进行扫描,然后显示扫描的结果。对于CheckService,每次音乐启动后都应立即启动CheckService,然后遍历媒体数据库中的歌曲,如果校正数据库中已经存有歌曲的校正信息,则跳过该歌曲,否则进行校正,然后将校正后的信息存放到数据库中。事实上,乱码校正和歌曲扫描可能是同时进行的,甚至扫描结束后乱码校正还没有完成,在扫描完成后可不用等待校正完成,而是直接从校正数据库中取校正信息,如果有则直接使用,如果没有则即时校正。
- 歌曲下载到本地后,歌曲下载后,由于是新增加的歌曲,校正数据库中肯定没有校正后的信息,此时应启动校正服务进行校正
- 媒体数据库有变化时(增加或删除歌曲),可通过注册ContentObserver的方式监听媒体数据的变化,当有新的歌曲,启动校正;当有歌曲被删除后,应将该首歌曲对于的校正信息从校正数据中删除,否则校正数据库会越来越大。
音乐启动时校正流程如下:
st=>start: 音乐启动
operStart=>operation: 启动校正服务
st->operStart
operLoop=>operation: 遍历媒体数据库
operStart->operLoop
condIsCheck=>condition: 已经校正?
operLoop->condIsCheck
operSkip=>operation: 跳过
operCheck=>operation: 进行校正
condIsCheck(yes)->operCheck
condIsCheck(no)->operSkip
operSave=>operation: 保存到校正数据库
operCheck->operSave
endCheck=>end: 结束校正
operSave->endCheck
音乐扫描时:
st=>start: 启动扫描
operHandle=>operation: 处理歌曲
st->operHandle
condIsCheck=>condition: 校正数据中有信息?
operHandle->condIsCheck
operCheck=>operation: 启动服务进行校正
operSave=>operation: 保存到校正数据库
operUse=>operation: 使用校正后的信息
condIsCheck(yes)->operUse
condIsCheck(no)->operCheck->operSave->operUse
operShow=>operation: 显示扫描结果
operUse->operShow
四. 格式识别参考代码
github有个项目id3tag
package cn.nubia.metedatacode;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/** * UTF-16 big endian FE FF > UTF-16 little endian FF FE > UTF-32 big endian 00 00 FE FF > UTF-32 little endian FF FE 00 00 > UTF-8 little endian EF BB BF */
public class CodeCheck {
public static String getStringByteArray(byte[] array)
throws UnsupportedEncodingException {
String retVal = null;
if (array.length < 3) {
return null;
}
if (isUnicode16LittleEndian(array) && Charset.isSupported("UTF-16LE")) {
retVal = new String(array, "UTF-16LE");
} else if (isUnicode16BigEndian(array) && Charset.isSupported("UTF-16BE")) {
retVal = new String(array, "UTF-16BE");
} else if (isUnicode32LittleEndian(array) && Charset.isSupported("UTF-32LE")) {
retVal = new String(array, "UTF-32LE");
} else if (isUnicode32BigEndian(array) && Charset.isSupported("UTF-32BE")) {
retVal = new String(array, "UTF-32BE");
} else if (isUTF8(array) && Charset.isSupported("UTF-8")) {
retVal = new String(array, "UTF-8");
} else if (isUTF8WithoutBom(array) && Charset.isSupported("UTF-8")) {
retVal = new String(array, "UTF-8");
} else if (isGBK(array) && Charset.isSupported("GBK")) {
retVal = new String(array, "GBK");
} else if (isBig5(array) && Charset.isSupported("Big5")) {
retVal = new String(array, "Big5");
} else {
retVal = new String(array);
}
byte[] data = retVal.getBytes();
int i = 0;
for(; i < data.length; i++){
if(data[i] == 0x00){
break;
}
}
return new String(data,0,i);
}
/** * UTF-16 little endian, 采用双字节或四字节编码,头部为两个字节,分别为: * 0xFF(255),0xFE(254) * @param content * @return */
public static boolean isUnicode16LittleEndian(byte[] content) {
int high, low;
if (content == null || content.length < 2)
return false;
high = content[0] & 0xFF;
low = content[1] & 0xFF;
if (high == 255 && low == 254)
return true;
return false;
}
/** * UTF-16 big endian, 采用双字节或四字节编码,头部为两个字节,分别为: * 0xFE(254),0xFF(255) * @param content * @return */
public static boolean isUnicode16BigEndian(byte[] content) {
int high, low;
if (content == null || content.length < 2)
return false;
high = content[0] & 0xFF;
low = content[1] & 0xFF;
if (high == 254 && low == 255)
return true;
return false;
}
/** * UTF-32 litter endian, 采用四字节编码,头部为四个字节,分别为: * 0x00,0x00,0xFE(254),0xFF(255) * @param content * @return */
public static boolean isUnicode32LittleEndian(byte[] content) {
int b1, b2, b3, b4;
if (content == null || content.length < 4)
return false;
b1 = content[0] & 0xFF;
b2 = content[1] & 0xFF;
b3 = content[2] & 0xFF;
b4 = content[3] & 0xFF;
if (b1 == 0 && b2 == 0 && b3 == 255 && b4 == 254)
return true;
return false;
}
/** * UTF-32 big endian, 采用四字节编码,头部为四个字节,分别为: * 0xFF(255),0xFE(254),0x00,0x00, * @param content * @return */
public static boolean isUnicode32BigEndian(byte[] content) {
int b1, b2, b3, b4;
if (content == null || content.length < 4)
return false;
b1 = content[0] & 0xFF;
b2 = content[1] & 0xFF;
b3 = content[2] & 0xFF;
b4 = content[3] & 0xFF;
if (b1 == 0 && b2 == 0 && b3 == 254 && b4 == 255)
return true;
return false;
}
/** * big5采用双字节编码,头部有两个字节,分别为 * 高位:0xA1-0xF9 * 低位:0x40-0x7E、0xA1-0xFE * @param content * @return */
public static boolean isBig5(byte[] content) {
if (content == null || content.length < 2)
return false;
int high = content[0] & 0xFF;
int low = content[1] & 0xFF;
if (high >= 0xA1
&& high <= 0xF9
&& ((low >= 0x40 && low <= 0x7E) || (low >= 0xA1 && low <= 0xFE)))
return true;
return false;
}
/** * UTF-8编码采用3个字节的头,这3个字节分别为: * 0xEF(239) 0xBB(187) 0xBF(191) * UTF-8采用不定长编码,英文为单字节编码,中文为3字节编码 * UNICODE转UTF-8规则如下: * 1. Unicode的编码范围为0000-007F, 对应的UTF8为0XXX XXXX * 2. Unicode的编码范围为0080-07FF, 对应的UTF8为110XXXXX 10XXXXXX * 3. Unicode的编码范围为0800-FFFF, 对应的UTF8为1110XXXX 10XXXXXX 10XXXXXX * @param content * @return */
public static boolean isUTF8(byte[] content) {
int high, low, lower;
if (content == null || content.length < 3)
return false;
high = content[0] & 0xFF;
low = content[1] & 0xFF;
lower = content[2] & 0xFF;
if (high == 239 && low == 187 && lower == 191)
return true;
return false;
}
public static boolean isUTF8WithoutBom(byte[] content) {
int i = 0;
while (i < content.length) {
int startByte = content[i] & 0xFF;
if (startByte <= 127) {
i++;
continue;
}
if (startByte <= 223 && startByte >= 192) {
if (i + 1 >= content.length)
return false;
if ((content[i + 1] & 0xFF) <= 191
&& (content[i + 1] & 0xFF) >= 128) {
i += 2;
continue;
}
return false;
} else if (startByte <= 239 && startByte >= 224) {
if (i + 2 >= content.length)
return false;
if ((content[i + 1] & 0xFF) <= 191
&& (content[i + 1] & 0xFF) >= 128
&& (content[i + 2] & 0xFF) <= 191
&& (content[i + 2] & 0xFF) >= 128) {
i += 3;
continue;
}
return false;
} else if (startByte <= 247 && startByte >= 240) {
if (i + 3 >= content.length)
return false;
if ((content[i + 1] & 0xFF) <= 191
&& (content[i + 1] & 0xFF) >= 128
&& (content[i + 2] & 0xFF) <= 191
&& (content[i + 2] & 0xFF) >= 128
&& (content[i + 3] & 0xFF) <= 191
&& (content[i + 3] & 0xFF) >= 128) {
i += 4;
continue;
}
return false;
} else if (startByte <= 251 && startByte >= 248) {
if (i + 4 >= content.length)
return false;
if ((content[i + 1] & 0xFF) <= 191
&& (content[i + 1] & 0xFF) >= 128
&& (content[i + 2] & 0xFF) <= 191
&& (content[i + 2] & 0xFF) >= 128
&& (content[i + 3] & 0xFF) <= 191
&& (content[i + 3] & 0xFF) >= 128
&& (content[i + 4] & 0xFF) <= 191
&& (content[i + 4] & 0xFF) >= 128) {
i += 5;
continue;
}
return false;
} else if (startByte <= 253 && startByte >= 252) {
if (i + 5 >= content.length)
return false;
if ((content[i + 1] & 0xFF) <= 191
&& (content[i + 1] & 0xFF) >= 128
&& (content[i + 2] & 0xFF) <= 191
&& (content[i + 2] & 0xFF) >= 128
&& (content[i + 3] & 0xFF) <= 191
&& (content[i + 3] & 0xFF) >= 128
&& (content[i + 4] & 0xFF) <= 191
&& (content[i + 4] & 0xFF) >= 128
&& (content[i + 5] & 0xFF) <= 191
&& (content[i + 5] & 0xFF) >= 128) {
i += 6;
continue;
}
return false;
} else
return false;
}
return true;
}
/** * GBK对汉字采用双字节编码,对英文、数字等采用单字节编码 * GBK 的中文编码范围为8140-FEFE(不包括XXFF),即: * 高位:0x81-0xFE * 低位:0x40-0x7E、0x80-0xFE * @param content * @return */
public static boolean isGBK(byte[] content) {
int i = 0;
while (i < content.length) {
int high = content[i] & 0xFF;
if (high <= 127) {
i++;
continue;
}
if (i + 1 >= content.length)
return false;
int low = content[i + 1] & 0xFF;
if (high >= 0x81 && high <= 0xFE && ((low >= 0x40 && low <= 0x7E) || (low >= 0x80 && low <= 0xFE))) {
i += 2;
continue;
} else
return false;
}
return true;
}
}