天天看點

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾

簡述

在 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;
}
           

程式截圖

檔案一:

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾

檔案二:

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾

從上述圖檔看來,檔案一和檔案二都帶有擴充資訊(附加資訊) , 可以根據 nFmtLength 是否為18 ,而上兩個檔案中nFmtLength 的值為18 确實有擴充資訊 ,,而擴充資訊包含了兩個資料字段,一個是擴充域大小(占兩個位元組),另一個是擴充域資訊資料(大小不定),從圖上可以看出nFmtLength的值為18,而Fmt塊的大小為16,是以多出的2個位元組即為擴充域大小,從圖中可以看出nAppendMessage值為0,表示沒有擴充資訊(也可以通過nFmtLength - 18來計算,不過前提是 nFmtLength >= 18 , 具體可以看上述代碼),也就無需讀取擴充資訊 , 可以看出圖中AppendMessageData的值為NULL。

同時檔案一和檔案二的nFmtLength都為18,包含了擴充資訊中的擴充域大小,同時特别注意兩個檔案都包含了Fact塊。

檔案三:

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾

檔案四:

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
從檔案三和檔案四看來,nFmtLength的值為16 , 表示沒有擴充資訊(附加資訊) ,這兩個wav音頻檔案沒有包含擴充資訊,也沒有Fact塊。

檔案五:

Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾
Qt 之 解析wav檔案的頭資訊(詳細分析、對比不同wav檔案的資料)簡述代碼之路尾

從檔案五中可以看到 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 數組指派時遇到的一些問題 ,後面也将會單獨對這些問題進行論述,敬請期待。

Good Night !

wav音頻檔案下載下傳

繼續閱讀