【wav音訊解析】之wavread函式的C++實現
本文由三部分組成,第一部分背景介紹 —— 音訊型別及本文動機,第二部分類比matlab下wavread()函式的作用,第三部分則給出該函式的C++實現。
一 背景介紹
1.1 本文動機
1)所有wav音訊處理的基礎就是將wav格式的檔案解析出來,解析成陣列才能供我們去做後續的處理(fft等等)。
2)在matlab中直接有一個很好用的函式wavread(' test.wav'),輸入是wav音訊,輸出是陣列,如第二章所述。
3)一般的C++函式讀取出來的資料,格式如1.2節所述,然而不管是什麼格式,資料之間是可互相轉換的。
4) 我在解決問題的過程中,沒有發現一篇詳細的參考文獻。
鑑於此,本文將介紹如何用C++完全實現matlab的wavread函式,輸出資料格式一模一樣,在這個過程中,大家也可以領略檔案中資料的本質,及相互間的轉換關係。
1.2 音訊型別
RIFF全稱為資源互換檔案格式(ResourcesInterchange FileFormat),RIFF檔案是windows環境下大部分多媒體檔案遵循的一種檔案結構,RIFF檔案所包含的資料型別由該檔案的副檔名來標識,能以RIFF檔案儲存的資料包括:音訊視訊交錯格式資料(.AVI) 波形格式資料(.WAV) 點陣圖格式資料(.RDI) MIDI格式資料(.RMI)調色盤格式(.PAL)多媒體電影(.RMN)動畫游標(.ANI)其它RIFF檔案(.BND)。
Chunk是組成RIFF檔案的基本單元,它的基本結構如下:
struct chunk{
u32 id; //由4個ASCII字元組成,用以識別塊中所包含的資料。如:'RIFF' ,'LIST','fmt','data','WAV','AVI'等
u32 size; //塊大小,是儲存在data域中資料的長度,id與size域的大小則不包括在該值內
u8 dat[size]; //塊內容,資料以字(WORD)為單位排列,如果該資料結構長度是奇數,則最後添一個NULL位元組
};
1.3 wav音訊檔案
WAVE 檔案作為多媒體中使用的聲音波形檔案格式之一,它是以RIFF(Resource Interchange File Format)格式為標準的。每個WAVE檔案的頭四個位元組便是“RIFF”。同樣的,WAVE 檔案由檔案頭和資料體兩大部分組成。其中檔案頭又分為 RIFF/WAV 檔案標識段和聲音資料格式說明段兩部分。WAVE檔案各部分內容及格式見後文。
常見的聲音檔案主要有兩種,分別對應於單聲道(11.025KHz 取樣率、8Bit 的取樣值)和雙聲道(44.1KHz 取樣率、16Bit 的取樣值)。取樣率是指:聲音訊號在“模→數”轉換過程中單位時間內取樣的次數。取樣值是指每一次取樣週期
內聲音模擬訊號的積分值。
對於單聲道聲音檔案,取樣資料為八位的短整數(short int 00H-FFH);而對於雙聲道立體聲聲音檔案,每次取樣資料為一個16位的整數(int),高八位和低八位分別代表左右兩個聲道。
WAVE 檔案資料塊包含以脈衝編碼調製(PCM)格式表示的樣本。WAVE 檔案是由樣本組織而成的。在單聲道 WAVE 檔案中,聲道0代表左聲道,聲道1代表右聲道。在多聲道WAVE檔案中,樣本是交替出現的。
WAVE 檔案除了前面一小段檔案頭對資料組織進行說明之外,Data 塊就是聲音的原始取樣資料,WAVE 檔案雖然可以壓縮,但一般都使用不壓縮的格式。44.1KHz 取樣率、16Bit的解析度、雙聲道,所以WAVE可以儲存音質要求非常高的聲音檔案,CD 採用的也是這種格式,聲音方面的專家或是音樂發燒友們應該非常熟悉。但這種檔案的體積也非常大,以 44.1KHz 16bit 雙聲道的資料為例,一分鐘的聲音資料量為:4100*2byte*2channel*60s/1024/1024=10.09M 。所以不合適在網上傳送。
下面我們具體地分析 WAVE 檔案的格式
endian | field name | Size | |
big | ChunkID | 4 | 檔案頭標識,一般就是" RIFF" 四個字母 |
little | ChunkSize | 4 | 整個資料檔案的大小,不包括上面ID和Size本身 |
big | Format | 4 | 一般就是" WAVE" 四個字母 |
big | SubChunk1ID | 4 | 格式說明塊,本欄位一般就是"fmt " |
little | SubChunk1Size | 4 | 本資料塊的大小,不包括ID和Size欄位本身 |
little | AudioFormat | 2 | 音訊的格式說明 |
little | NumChannels | 2 | 聲道數 |
little | SampleRate | 4 | 取樣率 |
little | ByteRate | 4 | 位元率,每秒所需要的位元組數 |
little | BlockAlign | 2 | 資料塊對齊單元 |
little | BitsPerSample | 2 | 取樣時模數轉換的解析度 |
big | SubChunk2ID | 4 | 真正的聲音資料塊,本欄位一般是"data" |
little | SubChunk2Size | 4 | 本資料塊的大小,不包括ID和Size欄位本身 |
little | Data | N | 音訊的取樣資料 |
以下是對各個欄位的詳細解說:
ChunkID | 4bytes | ASCII 碼錶示的“RIFF”。(0x52494646) |
ChunkSize | 4bytes | 36+SubChunk2Size,或是 4 + ( 8 + SubChunk1Size ) + ( 8 + SubChunk2Size ), 這是整個資料塊的大小(不包括ChunkID和ChunkSize的大小) |
Format | 4bytes | ASCII 碼錶示的“WAVE”。(0x57415645) |
SubChunk1ID | 新的資料塊(格式資訊說明塊) ASCII 碼錶示的“fmt ”——最後是一個空格。(0x666d7420) | |
SubChunk1Size | 4bytes | 本塊資料的大小(對於PCM,值為16)。 |
AudioFormat | 2bytes | PCM = 1 (比如,線性取樣),如果是其它值的話,則可能是一些壓縮形式 |
NumChannels | 2bytes | 1 => 單聲道 | 2 => 雙聲道 |
SampleRate | 4bytes | 取樣率,如 8000,44100 等值 |
ByteRate | 4bytes | 等於: SampleRate * numChannels * BitsPerSample / 8 |
BlockAlign | 2bytes | 等於:NumChannels * BitsPerSample / 8 |
BitsPerSample | 2bytes | 取樣解析度,也就是每個樣本用幾位來表示,一般是 8bits 或是 16bits |
SubChunk2ID | 4bytes | 新資料塊,真正的聲音資料 ASCII 碼錶示的“data ”——最後是一個空格。(0x64617461) |
SubChunk2Size | 4bytes | 資料大小,即,其後跟著的取樣資料的大小。 |
Data | N bytes | 真正的聲音資料 |
對於Data塊,根據聲道數和取樣率的不同情況,佈局如下(每列代表8bits):
1). 8 Bit 單聲道:
取樣1 | 取樣2 |
資料1 | 資料2 |
2). 8 Bit 雙聲道
取樣1 | 取樣2 | ||
聲道1資料1 | 聲道2資料1 | 聲道1資料2 | 聲道2資料2 |
3). 16 Bit 單聲道:
取樣1 | 取樣2 | ||
資料1低位元組 | 資料1高位元組 | 資料1低位元組 | 資料1高位元組 |
4). 16 Bit 雙聲道
取樣1 | |||
聲道1資料1低位元組 | 聲道1資料1高位元組 | 聲道2資料1低位元組 | 聲道2資料1高位元組 |
取樣2 | |||
聲道1資料2低位元組 | 聲道1資料2高位元組 | 聲道2資料2低位元組 | 聲道2資料2高位元組 |
下面我們看一個具體的例子,wav音訊檔案如下:(十六進位制的形式)
52 49 46 46 24 08 00 00 57 41 56 45
66 6d 74 20 10 00 00 00 01 00 02 00
22 56 00 00 88 58 01 00 04 00 10 00
64 61 74 61 00 08 00 00 00 00 00 00
24 17 1e 3c 13 3c 14 16 18 34 23 3c 24 11 1a 0d
對應的分析如下圖所示:
舉例分析資料:形如 'FFFF' 為一個我們需要的完整的資料。如上圖中 sample3:3c 和 13是兩個數組合在一起是一個我們需要的數, 3c 13,但右端為大端,則應為 3c 13,十六進位制數3c按位轉換為2進製為0011 1100,同理13按位轉換為2進製為0001 0011,則連起來的16bits的二進位制數為0011 1100 0001 0011,那麼我們可以看到符號位為0,即為正數。
二 matlab中的wavread( )函式
wavread('testwav.wav' )
讀者試試看輸出。例如,取我的一個聲音檔案'testwav.wav',輸出的最後10個數據為:
-0.0001 -0.0001 -0.0002 -0.0003 -0.0002 -0.0002 -0.0002 -0.0003 -0.0002 -0.0002
2. wavread('testwav.wav','native')
讀者可以試試看輸出。我的'testwav.wav' 輸出的最後10個數據為:
-4 -2 -8 -9 -7 -8 -8 -11 -5 -7
1 和 2 的輸出資料之間的轉換公式為:-0.0002 = -7 / 32768 (其中32768 = 2 ^15,即2的15次冪。這是歸一化。因為編碼為16bits)
三 readwav的 C++實現
上面介紹了這麼多,我們來進入主題,怎麼用C++實現matlab中的wavread('testwav.wav')函式,且輸出一致。
3.1 編碼轉換規則
在介紹之前,我們需要了解這幾串資料之間的關係。本章節以test.wav檔案的資料為例來分析:
(1)該wave檔案的Data塊即原始取樣資料的最後20個數據是:
fc ff fe ff f8 ff f7 ff f9 ff f8 ff f8 ff f5 ff fb ff f9 ff
(2)在matlab中解析得到的最後10個數據是:
-0.0001 -0.0001 -0.0002 -0.0003 -0.0002 -0.0002 -0.0002 -0.0003 -0.0002 -0.0002
這兩組資料之間是原碼與補碼的關係,即(1)是原碼而(2)是補碼。
由資料(1)轉換為資料(2)的步驟是:先將(1)轉換為其補碼,再用補碼除以32768,則得到(2)。
原碼與補碼之間的轉換原則:
(2進位制形式的轉換):若原碼為正數,則補碼是其本身。若原碼為負數,則補碼為符號位不變,數值位按位取反,再加1。
(數值形式的轉換):若原碼為正數,則補碼是其本身。若原碼為負數,補碼 = 原碼 - 2^16。溫馨提示: 為了方便計算數值上有等價替換 2^16 = FFFF - 1。
為了更好的理解,舉例說明:
步驟一(每次讀16位元組):由於資料是從X0000到XFFFF的資料。以f9 ff為例,右端為大端,換言之,右端是高位,則應該是fff9。步驟二(轉換為補碼):按位轉換為二進位制形式為1111 1111 1111 1001(1位16進位制數值對應4位二進位制數值),該資料為原碼,轉換成帶符號的十進位制形式,先看符號位判斷其為負數,則補碼為FFF9 - FFFF -1 = -7。步驟三(歸一化):用補碼數值-7除以32768,取小數點後4位(四捨五入),則等於-0.0002,正確。
讀者可以試著用我的方法算一下(1)中的右起第3第4個數,是否對應等於(2)的右起第2個數。
3.2 C++實現
那麼C++實現,就是先讀取原始取樣資料,每次讀16位元組,然後將16位元組的16進位制數字轉化成十進位制數,再轉換成其補碼,並歸一化。轉換時注意大小端和符號問題。
參考文獻