簡述
在 Qt 之 WAV檔案解析 中我們對wav檔案的檔案頭中的資料進行了分析,在 Qt之實作錄音播放及raw(pcm)轉wav格式 中我們實作了錄音/播放功能,并将.raw格式的音頻檔案轉為wav格式檔案,那我們拿到一個wav檔案如何擷取檔案的具體資訊呢,這一篇将叙述對wav檔案的頭資訊進行解析。
注意
在看這篇文章前希望讀者看一下 Qt 之 WAV檔案解析 和 Qt之實作錄音播放及raw(pcm)轉wav格式 這兩篇文章, 本篇文章也是基于這兩篇的基礎上進行叙述,如果讀者對wav檔案的格式有了一定的了解,也可以直接閱讀。
代碼之路
在Qt 之 WAV檔案解析 中我們對wav檔案頭進行了詳細的介紹,如果不清楚的可以了解一下。wav的檔案頭其實就是一個資料結構,結構中儲存了一系列參數,那我們從wav檔案中一一解析出這些參數。
// wav檔案頭資訊結構
struct WAVFILEHEADER
{
// RIFF 頭;
char RiffName[];
unsigned long nRiffLength;
// 資料類型辨別符;
char WavName[];
// 格式塊中的塊頭;
char FmtName[];
unsigned long nFmtLength;
// 格式塊中的塊資料;
unsigned short nAudioFormat;
unsigned short nChannleNumber;
unsigned long nSampleRate;
unsigned long nBytesPerSecond;
unsigned short nBytesPerSample;
unsigned short nBitsPerSample;
// 附加資訊(可選),根據 nFmtLength 來判斷;
// 擴充域大小;
unsigned short nAppendMessage;
// 擴充域資訊;
char* AppendMessageData;
//Fact塊,可選字段,一般當wav檔案由某些軟體轉化而成,則包含該Chunk;
char FactName[];
unsigned long nFactLength;
char FactData[];
// 資料塊中的塊頭;
char DATANAME[];
unsigned long nDataLength;
// 以下是附加的一些計算資訊;
int fileDataSize; // 檔案音頻資料大小;
int fileHeaderSize; // 檔案頭大小;
int fileTotalSize; // 檔案總大小;
// 理論上應該将所有資料初始化,這裡隻初始化可選的資料;
WAVFILEHEADER()
{
nAppendMessage = ;
AppendMessageData = NULL;
strcpy(FactName, "");
nFactLength = ;
strcpy(FactData, "");
}
};
// 解析wav檔案的頭資訊;
bool anlysisWavFileHeader(QString fileName)
{
QFile fileInfo(fileName);
if (!fileInfo.open(QIODevice::ReadOnly))
{
return false;
}
WAVFILEHEADER WavFileHeader;
// 讀取 資源交換檔案标志 "RIFF";
fileInfo.read(WavFileHeader.RiffName, sizeof(WavFileHeader.RiffName));
// 讀取 RIFF 頭後位元組數;
fileInfo.read((char*)&WavFileHeader.nRiffLength, sizeof(WavFileHeader.nRiffLength));
// 讀取 波形檔案辨別符 "WAVE";
fileInfo.read(WavFileHeader.WavName, sizeof(WavFileHeader.WavName));
// 讀取 波形格式标志 "fmt ";
fileInfo.read(WavFileHeader.FmtName, sizeof(WavFileHeader.FmtName));
// 讀取 格式塊中塊資料大小;
fileInfo.read((char*)&WavFileHeader.nFmtLength, sizeof(WavFileHeader.nFmtLength));
// 讀取 格式種類;
fileInfo.read((char*)&WavFileHeader.nAudioFormat, sizeof(WavFileHeader.nAudioFormat));
// 讀取 音頻通道數目;
fileInfo.read((char*)&WavFileHeader.nChannleNumber, sizeof(WavFileHeader.nChannleNumber));
// 讀取 采樣頻率;
fileInfo.read((char*)&WavFileHeader.nSampleRate, sizeof(WavFileHeader.nSampleRate));
// 讀取 波形資料傳輸速率;
fileInfo.read((char*)&WavFileHeader.nBytesPerSecond, sizeof(WavFileHeader.nBytesPerSecond));
// 讀取 資料塊對齊機關;
fileInfo.read((char*)&WavFileHeader.nBytesPerSample, sizeof(WavFileHeader.nBytesPerSample));
// 讀取 每次采樣得到的樣本資料位數值;
fileInfo.read((char*)&WavFileHeader.nBitsPerSample, sizeof(WavFileHeader.nBitsPerSample));
// 根據格式塊中塊資料大小,判斷是否有附加資訊;
QString strAppendMessageData; // 儲存擴充域中的擴充資訊;
if (WavFileHeader.nFmtLength >= )
{
// 讀取附加資訊占兩個位元組;
fileInfo.read((char*)&WavFileHeader.nAppendMessage, sizeof(WavFileHeader.nAppendMessage));
// 這裡 特别注意 nFmtLength 一般情況下是 16 或者18 ,但是有一個wav檔案 nFmtLength 為50;
// 說明我們讀取完fmt格式塊後面有附加資訊,上面一行代碼讀取了兩個位元組資料
// 這兩個位元組即為擴充域的大小,而剩餘的 50 - 18 = 32位元組即為擴充域中的擴充資訊;
// 對于擴充域中儲存了什麼格式的資料暫時無法得知,先用char型數組儲存;
// 這裡 擴充域大小 可以通過 WavFileHeader.nAppendMessage (從檔案中讀取的擴充域大小) 也可以通過 nFmtLength(格式塊長度) - 18 得到;
int appendMessageLength = WavFileHeader.nFmtLength - ;
WavFileHeader.AppendMessageData = new char[appendMessageLength];
fileInfo.read(WavFileHeader.AppendMessageData, appendMessageLength);
// 這裡也可以在末尾加字元結束符檢視資料,但是現在不确定擴充資訊的具體格式;
//WavFileHeader.AppendMessageData[appendMessageLength] = '\0';
// 轉成QString 檢視擴充資訊資料;
strAppendMessageData = QString(WavFileHeader.AppendMessageData);
}
// 由于Fact塊為可選,可能存在,是以需要判斷;
char chunkName[];
fileInfo.read(chunkName, sizeof(chunkName) - );
// 需要加上字元結束符 '\0',否則轉成QString會出錯,通過strlen來計算chunkName的字元長度也會出錯。
chunkName[] = '\0';
QString strChunkName(chunkName);
if (strChunkName.compare("fact") == )
{
// 存在fact塊,讀取資料;
strcpy(WavFileHeader.FactName, chunkName);
// 讀取fact塊長度;
fileInfo.read((char*)&WavFileHeader.nFactLength, sizeof(WavFileHeader.nFactLength));
// 讀取fact塊資料;
fileInfo.read(WavFileHeader.FactData, sizeof(WavFileHeader.FactData));
// 存在Fact塊 , 讀取 資料塊辨別符;
fileInfo.read(WavFileHeader.DATANAME, sizeof(WavFileHeader.DATANAME));
}
else
{
// 不存在Fact塊,直接指派;
strcpy(WavFileHeader.DATANAME, chunkName);
}
// 讀取 資料塊大小;
fileInfo.read((char*)&WavFileHeader.nDataLength, sizeof(WavFileHeader.nDataLength));
// 讀取 音頻資料大小;
WavFileHeader.fileDataSize = fileInfo.readAll().size();
// 檔案總大小;
WavFileHeader.fileTotalSize = WavFileHeader.nRiffLength + ;
//檔案頭大小;
WavFileHeader.fileHeaderSize = WavFileHeader.fileTotalSize - WavFileHeader.fileDataSize;
fileInfo.close();
return true;
}
程式截圖
檔案一:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyM3QTN0ATMwEjMxATM2EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
檔案二:
從上述圖檔看來,檔案一和檔案二都帶有擴充資訊(附加資訊) , 可以根據 nFmtLength 是否為18 ,而上兩個檔案中nFmtLength 的值為18 确實有擴充資訊 ,,而擴充資訊包含了兩個資料字段,一個是擴充域大小(占兩個位元組),另一個是擴充域資訊資料(大小不定),從圖上可以看出nFmtLength的值為18,而Fmt塊的大小為16,是以多出的2個位元組即為擴充域大小,從圖中可以看出nAppendMessage值為0,表示沒有擴充資訊(也可以通過nFmtLength - 18來計算,不過前提是 nFmtLength >= 18 , 具體可以看上述代碼),也就無需讀取擴充資訊 , 可以看出圖中AppendMessageData的值為NULL。
同時檔案一和檔案二的nFmtLength都為18,包含了擴充資訊中的擴充域大小,同時特别注意兩個檔案都包含了Fact塊。
檔案三:
檔案四:
從檔案三和檔案四看來,nFmtLength的值為16 , 表示沒有擴充資訊(附加資訊) ,這兩個wav音頻檔案沒有包含擴充資訊,也沒有Fact塊。
檔案五:
從檔案五中可以看到 nFmtLength 的值為50 , 剛開始看到這個值感覺這個檔案可能已經損壞或者有問題,因為之前查閱一些資料發現 一般nFmtLength 的值為 16或者18 (為18 時會包含擴充資訊),但此時數值為 50 ,這裡我不禁疑惑,後來我想想 擴充資訊中包含 擴充域大小和擴充域資訊資料,那麼擴充域大小占兩個位元組,那麼 50 - 16 - 2 = 32 位元組,多出來的32 位元組即為擴充域資訊資料,而用代碼解析出來的資料也有問題,後來分析是因為我沒有将這32位元組資料讀取出來,而是将這32個位元組的資料指派給了擴充資訊後面的資料,導緻擴充資訊後面的Fact塊 和Data塊解析有問題,是以再次修改代碼加上判斷是否存在擴充域資訊資料。
現在看檔案五右邊這張圖中我們發現 nAppendMessage 的值為 32 ,也驗證了多出來的 32位元組的擴充域資訊資料 , 由于不知道擴充資訊的資料結構,暫時先用char型數組接收資料, AppendMessageData現在有了資料,但是顯示亂碼,這裡我們就無需去分析擴充資訊中的資料了,由于擴充資訊是因為一些軟體自己生成的,是以我們隻要将擴充資訊讀取出來即可,也避免後面的資料讀取出錯。
綜合分析
同時注意檔案五不僅包含了擴充資訊,也包含了擴充域資訊資料,同時也存在Fact資料塊,是以綜合這幾個檔案資料對比,我們可以初步猜測包含了擴充資訊就會存在Fact資料塊,但是也不能完全斷定,還需繼續研究,最好是通過代碼解析得出具體資料。擴充資訊主要由一些軟體制成的wav格式中包含,具體有何意義有待研究。
頭資訊資料中nRiffLength這個值代表 RIFF 塊後位元組數也就是 整個檔案大小 - 8 ,經過對比這個值也是正确的。從檔案分析的結果中我們可以看到各個音頻檔案的各項參數。通過不同檔案的對比,主要的三個參數:聲道數、采樣頻率和采樣位數 (nChannleNumber、nSampleRate和nBitsPerSample),都不一緻,這幾個值也決定了音頻檔案的音質,檔案大小等屬性。
同時我們觀察 nDataLength 和 fileDataSize 這兩個參數 : nDataLength是wav檔案頭中記錄wav檔案中實際音頻資料所占的大小 , 而 fileDataSize 從代碼中可以看出是在讀取完檔案頭,後面資料的大小,而檔案頭後的資料也就是實際的音頻資料,看檔案一、檔案二和檔案五 中 nDataLength 和 fileDataSize這兩個值并不相等,目前猜測可能是由于擴充資訊的原因,從檔案三和檔案四中我們看到這兩個值是相等的,而且檔案三和檔案四并不包含擴充資訊,但是也不能完全斷定,還需繼續研究,進一步得到确切的論證。
尾
我在剛開始解析wav檔案時都是按照wav檔案的标準格式(即不包含擴充資訊 和 Fact塊)去解析頭資訊,而測試的wav檔案都是用 上一篇文章中 通過QAudioInput 類生成 .raw檔案再轉成 .wav檔案,而自己生成的.wav檔案的檔案頭都是自己添加的,而且不包含擴充資訊 和 Fact塊,是以用代碼解析過程中并沒有遇到問題,後來用了網上下載下傳的一些.wav檔案,發現解析出了問題。
回到上圖中,檔案三和檔案四為标準的wav格式,是以解析沒有問題,而檔案一和檔案二 包含了擴充資訊 和 Fact塊 , 導緻解析出現了問題, 這裡我通過判斷了nFmtLength 的長度是否為18得出是否包含擴充資訊,而此時我隻是讀了兩個位元組的 擴充域大小 , 對于檔案一和檔案二 中不包含 擴充域資訊資料是沒有問題的,下面我又判斷下面的字段是否是”fact”得出是否包含Fact塊,好了檔案一和檔案二解析資料也都正确,這下我以為已經成功了。
接着,我又試了檔案五,發現nFmtLength這個字段值為 50 , 除去擴充域大小(2個位元組)發現多出了32( = 50 - 16 - 2 )位元組,而nAppendMessage 的值也是32,經過分析 這多出的 32 位元組資料即為 擴充域資訊資料 。後面在通過 nFmtLength >= 18 來得出是否包含擴充域大小,再通過nFmtLength - 18 > 0 來得出是否包含擴充域資訊資料(其實這裡也可以通過nAppendMessage的值來判斷,具體代碼中也給出了詳細的注釋)。
以上是我寫這篇文章時所盡經曆的整個過程,本以為解析過程很簡單,但是真正去做時卻遇到了各種問題,這也是缺乏對wav檔案格式的認知。整篇文章的内容以及代碼也是經過了反複修改,通過對wav檔案頭的解析也讓我對wav檔案有了進一步的認識,同時也發現了之前的文章 Qt 之 WAV檔案解析 一文中的一些錯誤,我也做了進一步的修改。
文章文字叙述較多,詳細地講解了解析過程中遇到的問題以及解決辦法。整篇文章也是花了很長時間來完成,希望讀者能夠認真仔細看完(不過看之前最好看一下 Qt 之 WAV檔案解析 這篇文章),也希望能夠多多支援。我相信看完後對wav檔案就應該有了一定的認識,對後面如何處理wav檔案就好辦多了。後面會繼續講述Qt音頻處理相關的知識。同時在寫這篇部落格時發現了給char 數組指派時遇到的一些問題 ,後面也将會單獨對這些問題進行論述,敬請期待。