采樣和量化
首先需要明確的兩個概念,“采樣”和“量化”。對於給定的一個波形,采樣是從時間上將連續變成離散的過程,而采樣得到的值,可能還是不能夠用給定的位寬(比如8bit)來表示,這就需要經過量化,即從我們能夠表示的離散值里面找一個跟采樣值接近的值,近似地表示它。
一般來說,量化是模擬音頻到數字音頻(PCM)過程中產生誤差的唯一一個地方。
下面我們舉個例子來說明,首先用matlab生成一個正弦波,,由於
,所以這個波形的周期
然后以20的采樣周期采樣得到圖上的16個藍色點。
[0, 9.092974268, -7.568024953, -2.794154982, 9.893582466, -5.440211109, -5.36572918, 9.906073557, -2.879033167, -7.509872468, 9.129452507, -0.088513093, -9.05578362, 7.625584505, 2.709057883, -9.880316241]
這些小數存儲時要占用大量的空間,因此我們要通過量化,將其舍入到近似的整數,這樣采樣值就能用一個±16范圍的整數來存儲(5bit)。
1 x=0:1:300; 2 y=10*sin(x/10); 3 plot(x,y,'r') 4 axis([0,300,-12,12]); 5 set(gca, 'XTickMode','manual','XTick',[0:20:300]); 6 set(gca,'YTickMode','manual','YTick',[-10:1:10]);grid 7 hold on 8 a=0:20:300; 9 b=10*sin(a/10); 10 plot(a,b,'*')
以上就是一個簡單的采樣和量化過程。
根據采樣定理,用大於信號最高頻率兩倍的頻率,對周期信號進行采樣,可以保證完全重構原始信號。由於G711主要用於傳遞話音,而人聲最大頻率一般在3.4kHz,所以只要以8k的采樣頻率對人聲進行采樣,就可以保證完全還原原始聲音。
而人耳朵能夠感知的聲音頻率在20kHz范圍內,所以只要以大於40kHz頻率采樣,就可以完全重建原始聲音。我們常常能見到44.1kHz采樣的音樂文件,甚至更高采樣頻率,也是由於這個道理。之所以會取一個大於40k的采樣頻率比如44.1k、48k甚至更高的96k,我認為有以下幾個原因:
1)實際音頻的頻譜不是帶寬限制的,在帶外還有高頻信號。因此我們需要先經過一個低通濾波器將高頻信號濾掉。而實際的低通濾波器不是完整的在截止頻率將信號截斷,而是一個很陡峭的曲線,所以留出了一些余量保證濾除帶外信號后不影響帶內信號。
2)采樣之后需要經過量化,這帶來了一些誤差,重建出來的音頻和原始的模擬音頻有微小區別,通過增加量化深度或者采樣頻率能減少這種誤差。
3)實際的采樣窗口不是無限長的,加窗操作引入了一些大於奈奎斯特頻率的信號,導致頻率出現了混疊,影響了原始信號的恢復值。
4)在音頻制作過程中采用96k、192k甚至更高,可以滿足一些后期處理的需要。而播放端播放96k的音頻未必會比44.1k有更好的效果,兩者的區別可能更多來自於前3條的原因。
G711壓擴算法
G711算法采用8kHz采樣率,有A-law和μ-law兩種壓擴方式,分別是將13bit和14bit編碼為8bit,因此G711固定碼率是8kHz*8bit=64kbps。兩者都是對數變換,A-law更加方便計算機處理。μ-law提供了略微高一些的動態范圍,但代價是對於弱信號的量化誤差相對A-law高一些。兩者均采用對數變換的原因也正是由於人耳對於聲音的感知不是線性變化而是對數型變化的特性。
下面分別介紹兩種算法。
A-law的公式如下,一般采用A=87.6
畫出圖來則是如下圖,用x表示輸入的采樣值,F(x)表示通過A-law變換后的采樣值,y是對F(x)進行量化后的采樣值。
由此可見在輸入的x為高值的時候,F(x)的變化是緩慢的,有較大范圍的x對應的F(x)最終被量化為同一個y,精度較低。相反在低聲強區域,也就是x為低值的時候,F(x)的變化很劇烈,有較少的不同x對應的F(x)被量化為同一個y。意思就是說在聲音比較小的區域,精度較高,便於區分,而聲音比較大的區域,精度不是那么高。
μ-law的公式如下,μ取值一般為255
和A-law畫在同一個坐標軸中就能發現A-law在低強度信號下,精度要稍微高一些。
以上是兩種算法的連續條件下的計算公式,實際應用中,我們確實可以用浮點數計算的方式把F(x)結果計算出來,然后進行量化,但是這樣一來計算量會比較大,實際上對於A-law(A=87.6時),是采用13折線近似的方式來計算的,而μ-law(μ=255時)則是15段折線近似的方式。
A-law如下表計算,第一列是采樣點,共13bit,最高位為符號位。對於前兩行,折線斜率均為1/2,跟負半段的相應區域位於同一段折線上,對於3到8行,斜率分別是1/4到1/128,共6段折線,加上負半段對應的6段折線,總共13段折線,這就是所謂的A-law十三段折線法
對應的解碼公式則是
網上有G711的源碼,我們可以從中學到一些東西。

1 #define SIGN_BIT (0x80) /* Sign bit for a A-law byte. */ 2 #define QUANT_MASK (0xf) /* Quantization field mask. */ 3 #define NSEGS (8) /* Number of A-law segments. */ 4 #define SEG_SHIFT (4) /* Left shift for segment number. */ 5 #define SEG_MASK (0x70) /* Segment field mask. */ 6 #define BIAS (0x84) /* Bias for linear code. */ 7 8 static const int16_t seg_uend[8] = { 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF }; 9 static const int16_t seg_aend[8] = { 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF };; 10 11 static int16_t search(int16_t val, const int16_t *table, int16_t size) 12 { 13 int i; 14 15 for (i = 0; i < size; i++) { 16 if (val <= *table++) 17 return (i); 18 } 19 return (size); 20 } 21 22 static uint8_t linear2alaw(int16_t pcm_val) /* 2's complement (16-bit range) */ 23 { 24 int16_t mask; 25 int16_t seg; 26 uint8_t aval; 27 28 pcm_val = pcm_val >> 3;//這里右移3位,因為采樣值是16bit,而A-law是13bit數據,存儲在高13位上,低3位被舍棄 29 30 if (pcm_val >= 0) { 31 mask = 0xD5;//二進制的11010101 32 } 33 else { 34 mask = 0x55;//二進制的01010101,與0xD5只有符號位的不同 35 pcm_val = -pcm_val - 1;//負數轉換為正數計算 36 } 37 38 seg = search(pcm_val, seg_aend, 8);//查找采樣值對應哪一段折線 39 40 if (seg >= 8) { 41 return (uint8_t)(0x7F);//越界時直接返回最大值 42 } 43 else { 44 //以下按照表格處理,低4位是數據,5~7位是指數,最高位是符號 45 aval = (uint8_t)seg << SEG_SHIFT; 46 if (seg < 2) 47 aval |= (pcm_val >> 1) & QUANT_MASK; 48 else 49 aval |= (pcm_val >> seg) & QUANT_MASK; 50 51 return (aval); 52 } 53 }
跟算法相關的部分我已經在注釋中說明了,這里令人困惑的一點是mask的作用,為此我把輸入x從-32768~32767范圍內的所有輸出值對應標在坐標格上(圖一左半邊應該是在0坐標以下,為表示方便取了其絕對值)
對比兩張圖可以發現不使用mask的時候就是原始的A-law壓擴算法的13段折線。而使用mask則把結果重新進行了排布。這樣做的好處是
1、結果都是正數,最高位取反是原來的符號位
2、mask實際上是異或操作,mask的奇數位是1,偶數位是0,異或操作是對奇數位取反,偶數位保留,還原時只需要再次異或0x55即可
3、將奇偶數位進行不同的操作,防止相鄰干擾(但后來看到u-law並沒有這樣的情況,所以暫時存疑,可能只是一種為計算機優化的策略)
相應的解碼函數如下(加0x8那里是經常會有的尾數+0.5操作,是一種四舍五入的方法):

1 static int16_t alaw2linear(uint8_t a_val) 2 { 3 int16_t t; 4 int16_t seg; 5 6 a_val ^= 0x55;//異或操作把mask還原 7 8 t = (a_val & QUANT_MASK) << 4;//取低4位,即上表中的abcd值,然后左移4位變成abcd0000 9 seg = ((unsigned)a_val & SEG_MASK) >> SEG_SHIFT;//取中間3位,指數部分 10 switch (seg) 11 { 12 case 0://表中第一行,abcd0000 -> abcd1000 13 t += 8; 14 break; 15 case 1://表中第二行,abcd0000 -> 1abcd1000 16 t += 0x108; 17 break; 18 default://表中其他行,abcd0000 -> 1abcd1000 的基礎上繼續左移 19 t += 0x108; 20 t <<= seg - 1; 21 break; 22 } 23 return ((a_val & SIGN_BIT) ? t : -t); 24 }
相應的μ-law的計算方法如下表。
本質上跟A-law的區別不大
u-law計算時先用0x84 - sample(小於0)或者sample + 0x84,然后對應每一段使用不同的移位值得到最終的8bit結果,畫圖如下。解碼是上述過程的反方向,代碼比A-law簡單好理解。

1 static int16_t ulaw2linear(uint8_t u_val) 2 { 3 int16_t t; 4 5 /* Complement to obtain normal u-law value. */ 6 u_val = ~u_val; 7 8 /* 9 * Extract and bias the quantization bits. Then 10 * shift up by the segment number and subtract out the bias. 11 */ 12 13 t = ((u_val & QUANT_MASK) << 3) + BIAS; 14 t <<= ((unsigned)u_val & SEG_MASK) >> SEG_SHIFT; 15 16 return ((u_val & SIGN_BIT) ? (BIAS - t) : (t - BIAS)); 17 } 18 19 static uint8_t linear2ulaw(int16_t pcm_val) /* 2's complement (16-bit range) */ 20 { 21 int16_t mask; 22 int16_t seg; 23 uint8_t uval; 24 25 /* Get the sign and the magnitude of the value. */ 26 if (pcm_val < 0) { 27 pcm_val = BIAS - pcm_val; 28 mask = 0x7F; 29 } 30 else { 31 pcm_val += BIAS; 32 mask = 0xFF; 33 } 34 35 /* Convert the scaled magnitude to segment number. */ 36 seg = search(pcm_val, seg_uend, 8); 37 38 /* 39 * Combine the sign, segment, quantization bits; 40 * and complement the code word. 41 */ 42 if (seg >= 8) /* out of range, return maximum value. */ 43 return (0x7F ^ mask); 44 else { 45 uval = (seg << 4) | ((pcm_val >> (seg + 3)) & 0xF); 46 return (uval ^ mask); 47 } 48 }
代碼里還有ulaw和alaw互相轉換的函數,由於兩者都是8bit,去除符號位就是128個值,直接用數組映射就實現了a-u互相轉換,這樣時間復雜度只有O(1)
總結
G711盡管是一種非常古老的話音編碼算法,原理和計算也比較簡單,但是其中用到的一些基本原理同樣在其他編碼算法中得到了應用,對其進行深入的了解有助於更好的理解其他的算法。
源代碼中關於移位運算,掩碼運算,我還不是完全的理解,只能根據自己的經驗進行一些猜測,之后會繼續學習,希望對這方面能有更深入的認識。
采樣和量化是編解碼的基礎知識,因此也對此進行了區分和強調。
主要參考文獻:
1、英文wiki的G711詞條,里面有對該算法的詳細描述
2、G711的標准算法源代碼,代碼利用了移位運算等進行了加速,比原始的浮點計算方式更快,適用於嵌入式設備等性能較為低下的設備。
https://github.com/quatanium/foscam-ios-sdk/blob/master/g726lib/g711.c