C語言解析WAV音頻檔案代碼地址: Github : github.com/CasterWx/c-wave-master目錄
- 前言
- 瞭解WAV音頻檔案
- 什麼是二進位檔案
- WAV的二進位格式解析
- C語言解析WAV音頻檔案
- 兩個細節
- 總結
在電腦中有著各式各樣的檔案,比如說EXE這種可執行檔,JPG這種圖片檔案,也有我們平時看的TXT,或者C,CPP,PHP等代碼檔案。
如果把這些檔案用記事本或者其他純文字編輯器開啟,會發現前面這類檔案開啟之後基本上都是亂碼,也就是非人類可讀的字元,而後面這類代碼或者TXT檔案開啟之後都是人類可讀的字串。
如果我們把這些檔案統一做一個分類,那麼前面的EXE,JPG之類的這種開啟之後都是我們看不懂的外星球文字的檔案叫做二進位檔案,而後面那些檔案可以稱為是文字檔。
後面那種分類是文字檔很好理解,畢竟都是我們認識的文本文字,但是前面的那些亂碼為什麼叫他二進位檔案呢?這些二進位檔案是怎麼被電腦識別的,為什麼這些亂碼就能被電腦識別,並且放出悠揚動聽的音樂或者栩栩如生的圖片呢?我們學編程,搞電腦的人能不能也自己寫一個程式把這些資料解析出來呢?請跟聽本專欄欄豬一起慢慢道來。
前言
我們將一步一步來瞭解C語言的一些基本庫的使用,以及如何使用這些庫來解析一個wav格式的音頻檔案,將其中的中繼資料(也就是該音頻檔案的一些屬性)提取出來。因此您需要有基本的電腦基礎知識以及瞭解C語言,最好還對音頻或者訊號處理感興趣。
瞭解WAV音頻檔案
下面是百度百科的解釋
WAV為微軟公司(Microsoft)開發的一種音效檔格式,它符合RIFF(Resource Interchange File Format)檔案規格,用於儲存Windows平台的音頻資訊資源,被Windows平台及其應用程式所廣泛支援,該格式也支援MSADPCM,CCITT A LAW等多種壓縮運演算法,支援多種音頻數字,取樣頻率和聲道,標準格式化的WAV檔案和CD格式一樣,也是44.1K的取樣頻率,16位量化數字,因此在音效檔品質和CD相差無幾! WAV開啟工具是WINDOWS的媒體播放器。
通常使用三個參數來表示聲音,量化位元,取樣頻率和採樣點振幅。量化位元分為8位,16位,24位三種,聲道有單聲道和立體聲之分,單聲道振幅資料為n1矩陣點,立體聲為n2矩陣點,取樣頻率一般有11025Hz(11kHz) ,22050Hz(22kHz)和44100Hz(44kHz) 三種,不過儘管音質出色,但在壓縮後的檔案體積過大!相對其他音頻格式而言是一個缺點,其檔案大小的計算方式為:WAV格式檔案所佔容量(B) = (取樣頻率 X量化位元X 聲道) X 時間 / 8 (位元組= 8bit) 每一分鐘WAV格式的音頻檔案的大小為10MB,其大小不隨音量大小及清晰度的變化而變化。
我們通常在各種音樂播放器中下載歌曲的時候會看到各種參數,比如說普通音質的碼流為128k,高品質是320k,還有無損的APE,FLAC等格式。還有的時候我們在使用各種音頻格式轉換工具中會遇到各種參數,比如說採樣率,量化精度,以及該音頻檔案是單聲道還是雙聲道等等。
我們現在都是聽MP3格式的音樂,WAV現在除了Windows的錄音機以外,基本上沒有地方會用了,為什麼還要用他來做樣本呢?這是因為WAV本質上是無壓縮的原始音頻檔案,而且他的檔案結構不算非常複雜,因此可以作為我們初學者的學習樣本格式。你可以按照同樣的思路自己去學習其他格式。
什麼是二進位檔案
二進位檔案,本質上就是一種使用二進位方式隱藏檔內容的檔案統稱,我們前面有講過使用記事本等工具開啟之後看到的是亂碼,那麼我們怎麼分析他呢,可以使用UltraEditor,HxD,C32Asm等等。比如我這裡使用HxD開啟Windows 7的關機音樂(C:\Windows\Media\Windows Shutdown.wav)就是這個樣子,左邊就是這個WAV音頻檔案的二進位表示,右邊則是這個位元字對應的ASCII表示,由於像00之類的數字在ASCII中並沒有有效映像來顯示,所以在這個介面的右邊顯示的就是一個點。而左邊這些52,49之類的數字分別對應什麼呢?其實這些位元字看似亂碼,其實都是有一定的規範的,只要我們或者我們電腦上面的應用程式瞭解這個規範,就可以按照這個規範去解讀它。
WAV的二進位格式解析
根據網路上的各種資料可以得知WAVE檔案本質上就是一種RIFF格式,它可以抽象成一顆樹(資料結構的一種)來看。
我們看到這張圖上面,從上到下分別對應著位元據在檔案中相對於起始位置的位移量。每一個格子對應一個欄位,field size表示每個欄位所佔據的大小,根據這個大小以及當前的位移量,我們也可以計算出下一個欄位的起始地址(位移量)。
接下來我們來解釋一下上面每個欄位的含義。根據RIFF的規範,整個WAV檔案的頂級chunk就是最頂上的ChunkID為RIFF的這個chunk,這也可以解釋為什麼之前那張圖片中我們可以看出wav檔案開頭都是RIFF幾個字母。而接下來的ChunkSize則表示這個chunk下的那些子chunk的大小,如果按照“樹結構”來理解,那麼每一個子chunk(Subchunk)則為樹的樹枝。而Format則為這個chunk的實際資料。
說白了一個chunk結構其實就是三個部分,第一個部分標識符用於說明這個chunk是存什麼內容的,第二個部分則是說明這個chunk的內容到底有多大,用於讓程式知道如果要找到下一個chunk該把地址位移多少去讀取,而第三個部分則是實際內容。
好了說完了頂級chunk,我們就來看看子chunk,第一個子chunk的Subchunk1ID在WAV檔案中恒定為fmt,表示該subchunk的內容為該WAV音頻檔案的一些中繼資料,也就是該WAV音訊一些格式資訊。比如說AudioFormat這個欄位一般為1,表示這個WAV音頻為PCM編碼。NumChannels則是該WAV音頻檔案的聲道數量。SampleRate則為採樣率,ByteRate則為採樣率。BlockAlign則是每個block的平均大小,它等於NumChannels * BitsPerSample/8,至於block是什麼,以及它的計算公式是怎麼得來的需要來看看另一個Subchunk。BitsPerSample則為每秒採樣位元,有的地方稱它為量化精度或者PCM位寬。(未考究)
另一個子chunk也就是Subchunk2ID是在WAV檔案中恒定為data,也就是這個WAV音頻檔案的實際音頻資料,說專業一點,這裡面儲存的是音訊採樣資料。但是我們的音頻如果是雙聲道,那麼實際上某一個採樣時刻採樣的資料是由左聲道和右聲道共同組成的。而這個共同組成的採樣我們把他成為block。前面有講到BlockAlign = NumChannels * BitsPerSample / 8,這個現在就很好理解了,至於為什麼末尾要除以8,這是因為電腦中是以8個位元表示一個位元組,所以要除以8來求出位元組數。
至於音訊持續長度,我們可以通過Subchunk2Size除以ByteRate,也就是實際音頻data的chunk總長度除以每秒位元組數得到持續多少秒。
C語言解析WAV音頻檔案
前面講了這麼多,現在問題來了,怎麼編程來實現解析上面所說的這些中繼資料呢。C語言基本的二進位檔案操作函數有fopen,fread等等。(注意是二進位檔案操作函數,所以我們不討論fgets,這是普通的文字檔操作函數)
fread是一個函數。從一個檔案流中讀資料,最多讀取count個項,每個項size個位元組,如果調用成功返回實際讀取到的項個數(小於或等於count),如果不成功或讀到檔案末尾返回 0。
它的函數原型為
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
而且C語言還有一種類型叫做結構體,它在記憶體中是順序儲存的。剛好我們也已經得知了WAV檔案在檔案中的順序以及該順序中每個部分對應的含義,那麼我們可以事先根據前面所說的WAV檔案結構來定義好一個struct,然後在main主函數中初始化這個struct,並且通過fread的第一個參數帶入初始化好的這個struct,那麼執行之後就會自動讀取該檔案,並且按照順序自動把這些中繼資料填充進了我們初始化好的struct中。我們便可以直接從struct中取到這些中繼資料了。
代碼如下:
wave.c
#include <stdio.h> #include <stdint.h> #include <stdlib.h> #include "wave.h" int main() { FILE *fp = NULL; Wav wav; RIFF_t riff; FMT_t fmt; Data_t data; fp = fopen("test.wav", "rb"); if (!fp) { printf("can't open audio file\n"); exit(1); } fread(&wav, 1, sizeof(wav), fp); riff = wav.riff; fmt = wav.fmt; data = wav.data; printf("ChunkID \t%c%c%c%c\n", riff.ChunkID[0], riff.ChunkID[1], riff.ChunkID[2], riff.ChunkID[3]); printf("ChunkSize \t%d\n", riff.ChunkSize); printf("Format \t\t%c%c%c%c\n", riff.Format[0], riff.Format[1], riff.Format[2], riff.Format[3]); printf("\n"); printf("Subchunk1ID \t%c%c%c%c\n", fmt.Subchunk1ID[0], fmt.Subchunk1ID[1], fmt.Subchunk1ID[2], fmt.Subchunk1ID[3]); printf("Subchunk1Size \t%d\n", fmt.Subchunk1Size); printf("AudioFormat \t%d\n", fmt.AudioFormat); printf("NumChannels \t%d\n", fmt.NumChannels); printf("SampleRate \t%d\n", fmt.SampleRate); printf("ByteRate \t%d\n", fmt.ByteRate); printf("BlockAlign \t%d\n", fmt.BlockAlign); printf("BitsPerSample \t%d\n", fmt.BitsPerSample); printf("\n"); printf("blockID \t%c%c%c%c\n", data.Subchunk2ID[0], data.Subchunk2ID[1], data.Subchunk2ID[2], data.Subchunk2ID[3]); printf("blockSize \t%d\n", data.Subchunk2Size); printf("\n"); printf("duration \t%d\n", data.Subchunk2Size / fmt.ByteRate); }
wave.h
typedef struct WAV_RIFF { /* chunk "riff" */ char ChunkID[4]; /* "RIFF" */ /* sub-chunk-size */ uint32_t ChunkSize; /* 36 + Subchunk2Size */ /* sub-chunk-data */ char Format[4]; /* "WAVE" */} RIFF_t;typedef struct WAV_FMT { /* sub-chunk "fmt" */ char Subchunk1ID[4]; /* "fmt " */ /* sub-chunk-size */ uint32_t Subchunk1Size; /* 16 for PCM */ /* sub-chunk-data */ uint16_t AudioFormat; /* PCM = 1*/ uint16_t NumChannels; /* Mono = 1, Stereo = 2, etc. */ uint32_t SampleRate; /* 8000, 44100, etc. */ uint32_t ByteRate; /* = SampleRate * NumChannels * BitsPerSample/8 */ uint16_t BlockAlign; /* = NumChannels * BitsPerSample/8 */ uint16_t BitsPerSample; /* 8bits, 16bits, etc. */} FMT_t;typedef struct WAV_data { /* sub-chunk "data" */ char Subchunk2ID[4]; /* "data" */ /* sub-chunk-size */ uint32_t Subchunk2Size; /* data size */ /* sub-chunk-data */// Data_block_t block;} Data_t;//typedef struct WAV_data_block {//} Data_block_t;typedef struct WAV_fotmat { RIFF_t riff; FMT_t fmt; Data_t data;} Wav;
執行結果
兩個細節
1、fopen的時候我們的mode要設定為"rb",r表示read,b表示binary,也就是二進位讀取方式。這一點是和讀取傳統的文字檔格式有所區別的。
2、struct類型裡面我用的是uint32_t等類型,而不是傳統的int,short等等,這是為了考慮到不同的編譯器,不同的平台下對於int類型分配的記憶體空間不一致的問題。而這些類型是由stdint.h標頭檔提供的,因此我們需要在頭部匯入它。
總結
其實任何位元據都是有著屬於它自己的解析規範,這就有點像我們學電腦網路的時候所說的“協議”,只要我們遵循這個規範或者“協議”,那麼我們就可以將該檔案真正隱含的資訊讀取出來。
我們這裡僅僅是讀取了一段WAV音頻檔案的中繼資料,沒有把它的data chunk,也就是實際音訊數字訊號讀取出來,因為這涉及到數模訊號的轉換等知識,超出了我們的研究範圍,
代碼參考