wav音频文件头动态解析--java语言

之前有处理过一些相对较为不常见的音频格式,也睬过很多坑,这里做一下简单记录。后面可能随着接触音频类型的增多做进一步更新,像之前有记录过包含LIST数据块的wav格式录音就是调试过程中发现遗漏点。
在此之前先整理一下常规音频文件头的基本结构,如下图:

可以看到在文件头中,不同位置的字节代表不同的数据块。相对来说,大部分情况一些数据块的信息是关注度不高的,像LIST数据块,而另一些诸如音频长度,格式,位长,采样率等等是关注度较高的,所以在处理过程中可以把需要的数据块定义成一个通用的结构,在解析后设置该结构对应的结果返回。对于wav文件来说,以chunk为基本存储单位,一个wav文件包含3个必要chunk和一个可选chunk,在文件中排列循序是:RIFF chunk ,Format chunk ,Fact chunk(可选),Data chunk。
所以在从前往后读取文件头并解析时,riff chunk和fm chunk时固定的。chunk划分如下:

根据其不同数据块的次序和长度以及字节长度,可以定义一份固定的riff和fm chunk的结构如下:
{ "name" : "riff_id", "index" : 0, "type" : "chars", "len" : 4, "value" : "RIFF" }, { "name" : "file_size", "index" : 1, "type" : "int", "len" : 4, "value" : 0 }, { "name" : "riff_type", "index" : 2, "type" : "chars", "len" : 4, "value" : "WAVE" }, { "name" : "fmt_hdr_id", "index" : 3, "type" : "chars", "len" : 4, "value" : "fmt " },
一,每一个数据块由一个binaryElement描述,包括name,insex,type,len,value等属性。其中type跟每个数据块的value类型相对应,影响在数据读取后的转化过程。
不同数据类型的转化过程如下做局部示例:
`/**
* byte转int
*
* @param b 字节数组
* @return 解析出来的数字
*/
public static int toInt(byte[] b) {
return b[0] & 0xff | (b[1] & 0xff) << 8 | (b[2] & 0xff) << 16
| (b[3] & 0xff) << 24;
}

/**
 * byte转long
 *
 * @param b 字节数组
 * @return 解析出来的数字
 */
public static long toUnsignedInt(byte[] b) {
    return  b[0] & 0xff  | (b[1] & 0xff) << 8 | (b[2] & 0xff) << 16
            | (b[3] << 24);
}

/**
 * byte转short
 *
 * @param b 字节数组
 * @return 解析出来的数字
 */
public static  short toShort(byte[] b) {
    return (short) (((b[1] << 8) | b[0] & 0xff));
}`

二,整体的解析过程如下:
先解析固定区域,riff和fmchunk,共11个数据块:
` //记录当前读取位置
int readCur = 0;

        byte[] tmp;
        for (int i = 0; i < FixedStrutNum; i++) {
            BinaryProtocolElement ele = headerStrut.get(i);
            if (ele != null) {
                tmp = new byte[ele.getLength()];
                stream.read(tmp);
                //转化未对应数据类型
                bytesToElement(tmp, ele, encoding);
                readCur += ele.getLength();
            }
        }`
  
  然后处理fmt的扩展区,如果有的话。该部分可通过fmchunk的长度判断是否存在扩展区。即该chunk的总长度 - 固定的字段长度16,若不为0则说明存在扩展区,需要跳过解析。
  `//处理fmt的扩展区 长度fmt_data_len - 16
        //无须该区段扩展信息,跳过即可
        int extLength = Integer.parseInt(
                headerStrut.get(WaveHeaderEleDefine.FMT_HDR_LEN.getIndex()).getValue())
                - PCM_FMT_LEN;
        if (extLength != 0) {
            //先获取扩展块数据的长度
            BinaryProtocolElement dataLenExt = new BinaryProtocolElement();
            dataLenExt.setDataType(ProtocolDataType.SHORT);
            dataLenExt.setLength(2);
            tmp = new byte[dataLenExt.getLength()];
            stream.read(tmp);
            bytesToElement(tmp, dataLenExt, encoding);
            readCur += extLength;

            //扩展区长度为0或22
            //如果为22需要手动跳过
            int extDataLength;
            if ((extDataLength = Integer.parseInt(dataLenExt.getValue())) != 0) {
                readCur += extDataLength;
                stream.skip(extDataLength);
            }
        }`


  接着判断是否有可选chunk--fack,可以通过后续data_tag标签数据块来判断,若后续DATA_TAG为FACT,则需要获取该chunk长度并跳过解析。
  `//根据data_tag判断是否有可选chunk fact
        tmp = new byte[dataTag.getLength()];
        stream.read(tmp);
        bytesToElement(tmp, dataTag, encoding);
        readCur += dataTag.getLength();

        //如果有fact域需要跳过该域重新解析tag
        //如果没有读取data_len即可
        if (TAG_CHUNK_FACT.equalsIgnoreCase(String.valueOf(dataTag.getValue()))) {
            tmp = new byte[dataLen.getLength()];
            stream.read(tmp);
            bytesToElement(tmp, dataLen, encoding);
            readCur += dataLen.getLength();


            //跳过fact chunk的data域
            readCur += Integer.parseInt(dataLen.getValue());
            stream.skip(Integer.parseInt(dataLen.getValue()));

            //解析data域的tag
            tmp = new byte[dataTag.getLength()];
            stream.read(tmp);
            bytesToElement(tmp, dataTag, encoding);
            readCur += dataTag.getLength();
        }`
  
  同理,需要校验后续chunk是否为LIST chunk,若是,也需跳过解析。
  `//针对格式转换过后的wav文件
        //存在LIST数据块保留一些格式转换的信息
        //此处数据暂时没有业务需要,解析出LIST数据块长度后跳过
        //LIST数据块结构包括 listSize listType listData
        //listSize占有4字节,listSize的值是listType的大小(即4个字节)加上listData的长度。不包括“LIST”和listSize的长度。
        if (TAG_CHUNK_LIST.equalsIgnoreCase(dataTag.getValue())) {
            //先解析出list_size
            BinaryProtocolElement listSize = new BinaryProtocolElement();
            listSize.setDataType(ProtocolDataType.INT);
            listSize.setLength(4);
            tmp = new byte[listSize.getLength()];
            stream.read(tmp);
            bytesToElement(tmp, listSize, encoding);
            readCur += extLength;

            //然后跳过listType和listData
            readCur += Integer.parseInt(listSize.getValue());
            stream.skip(Integer.parseInt(listSize.getValue()));

            //解析data域的tag
            tmp = new byte[dataTag.getLength()];
            stream.read(tmp);
            bytesToElement(tmp, dataTag, encoding);
            readCur += dataTag.getLength();
        }`

  最后,解析data chunk,获取音频数据长度并返回文件头长度(若有需要)
  `//没有获取到data标签
        //检测音频头信息是否有误
        //或者有错误移位
        if (!TAG_CHUNK_DATA.equalsIgnoreCase(dataTag.getValue())) {
            log.error("can not fetch the tag of data chunk, please check");
            return 0;
        }

        //读取data域的data_len
        tmp = new byte[dataLen.getLength()];
        stream.read(tmp);
        bytesToElement(tmp, dataLen, encoding);
        readCur += dataLen.getLength();
        //这里由于跳过对应扩展区,所以长度要调整一下
        headerStrut.get(WaveHeaderEleDefine.FMT_HDR_LEN.getIndex())
                .setValue(PCM_FMT_LEN + "");
        //最后返回文件头长度
        return readCur;`

参考资料:https://www.jianshu.com/p/02c4df08c51c
https://www.jianshu.com/p/02c4df08c51c

posted @ 2020-07-12 19:15  轻天  阅读(1070)  评论(1编辑  收藏  举报