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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM