之前有處理過一些相對較為不常見的音頻格式,也睬過很多坑,這里做一下簡單記錄。后面可能隨着接觸音頻類型的增多做進一步更新,像之前有記錄過包含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