C語言解析WAV音頻文件
代碼地址: Github : https://github.com/CasterWx/c-wave-master
目錄
在計算機中有着各式各樣的文件,比如說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,也就是實際音頻的數字信號讀取出來,因為這涉及到數模信號的轉換等知識,超出了我們的研究范圍,