Android播放器音乐信息乱码及其解决方法

使用电脑、手机登方式播放音乐时,经常会遇到音乐信息乱码的场景,涉及到的乱码信息包括歌曲名、专辑名、艺术家、流派等。

一. 基础知识

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. 整体逻辑

上图这是最基本的校正流程,如果乱码发生了播放器中,就要考虑到整体的流程。

需要考虑以下几个问题:

  1. 校正歌曲信息是耗时操作,采用何种方式进行校正乱码
  2. 校正后的信息保存在哪?
  3. 在哪些场景需要执行乱码校正
第一个问题:校正歌曲信息是耗时操作,采用何种方式进行校正乱码?

可新建一个校正服务(比如CheckService),在CheckService中启动线程执行乱码校正,CheckService的设计可参考Android原生的MediaScanService的架构。CheckService采用bindService的方式启动,并且将校正完成后的回调接口以IBinder格式的参数传入CheckService中,这样校正完毕后就可通知客户端乱码校正已完成。

第二个问题:校正后的信息保存在哪?

考虑到要进行校正的歌曲数量可能比较大,并且可能要将校正后的信息开放给别的应用使用,可将校正后的信息存放到数据库中,并自定义ContentProvider,将保存校正后的歌曲信息的数据库以URI的方式发布出去,便于其他的应用或组件调用。

第三个问题:在哪些场景需要执行乱码校正?
  1. 一般来说,播放器在第一次启动后需要进行扫描,然后显示扫描的结果。对于CheckService,每次音乐启动后都应立即启动CheckService,然后遍历媒体数据库中的歌曲,如果校正数据库中已经存有歌曲的校正信息,则跳过该歌曲,否则进行校正,然后将校正后的信息存放到数据库中。事实上,乱码校正和歌曲扫描可能是同时进行的,甚至扫描结束后乱码校正还没有完成,在扫描完成后可不用等待校正完成,而是直接从校正数据库中取校正信息,如果有则直接使用,如果没有则即时校正。
  2. 歌曲下载到本地后,歌曲下载后,由于是新增加的歌曲,校正数据库中肯定没有校正后的信息,此时应启动校正服务进行校正
  3. 媒体数据库有变化时(增加或删除歌曲),可通过注册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;
    }
}


    原文作者:Android
    原文地址: http://www.caiyeccy.com/android/2016/09/03/Android%E6%92%AD%E6%94%BE%E5%99%A8%E9%9F%B3%E4%B9%90%E4%BF%A1%E6%81%AF%E4%B9%B1%E7%A0%81%E5%8F%8A%E5%85%B6%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞