GSM是voip中較爲常見的一種編碼,壓縮率比很高,寫到wav文件每秒只佔用1.6k字節(接近於g729),是普通g711格式的五分之一,對錄音來說可節省大量磁盤空間。生成的wav文件,可能是不牽涉到專利的原因,在各種操作系統下都能夠播放。
1、使用IPP的codec
GSM的編解碼通常使用開源C代碼,入口文件是gsm_encode.c和gsm_decode.c,能用,但不夠優化。
我使用Intel公司著名的IPP庫,這個庫經過高度優化,cpu佔用很小。
實際使用中發現用IPP進行encode後rtp發給對方,對方軟電話無聲,解不出來。經過跟蹤,IPP編碼出來的33個字節,第1個字節的前半個字節爲0,應該爲gsm“魔術字”:0xd,加上這麼幾句即可:
IppEncode(... ..., outRawBuff);
unsigned char ch = outRawBuff[0];
ch = ch | 0xd0;
outRawBuff[0] = ch;
2、寫wav文件頭
關於gsm格式的wav文件格式,網上的文章很少,大多語焉不詳。
其實掌握要這2個要點就好:
首先,voip一般都是單聲道,8000採樣率,這個是基本。
然後,GSM encode後的一幀數據是32.5個字節,wav文件當然不能存儲半個字節,所以2幀組成一個數據塊,即65字節,剩下的都是枝節了:
... ...
WAVEFORMATEX m_wfExt;
int fmtLen = 20; // fmt塊長20字節
memcpy(FileHeader.dwRiff, "RIFF", 4);
memcpy(FileHeader.dwWave, "WAVE", 4);
memcpy(FileHeader.dwFormat, "fmt ", 4);
FileHeader.dwFormatLength = fmtLen; // 20
FileHeader.dwFileSize = contentLen + 20 + fmtLen;
wavFp.write((char*)&FileHeader, 20);
m_wfExt.nChannels = 1; // 單聲道
m_wfExt.nSamplesPerSec = 8000; // 採樣率
m_wfExt.wFormatTag = WAVE_FORMAT_GSM610;
m_wfExt.wBitsPerSample = 0; // 每個採樣的位數,gsm編碼顯然無法湊成整數,所以填0
m_wfExt.nBlockAlign = 65; // 塊對齊,即一個塊的字節數:1個塊就是2數據幀65字節
m_wfExt.nAvgBytesPerSec = 1625; // 評價每秒的字節數,1秒有50個採樣週期,2個採樣週期爲65字節,所以65*25=1625字節
m_wfExt.cbSize = 2; // 擴展字段長度,即佔2個字節
WORD wSamplesPerBlock = 320; // 擴展字段:每個塊的採樣數,對於GSM來說兩幀數據爲1塊,所以此處填320,即160x2
char fmtBuff[300];
memset(fmtBuff, 0, 300);
memcpy(fmtBuff, &m_wfExt, 18);
memcpy(fmtBuff + 18, &wSamplesPerBlock, 2);
wavFp.write(fmtBuff, fmtLen); // 寫入fmt塊
3、數據如何寫入wav文件?
上面說了,用gsm codec編碼或rtp收取到的一幀gsm數據是33字節,有效數據爲32.5字節,第一個字節的前4位是沒什麼用的GSM“魔術字”0xd,但如何將兩幀數據合併成65字節的塊呢?
最容易想到的辦法是將第一幀數據全部前移4bit,剩下半個字節,填充到下一幀的第一個字節,經試驗證明不行,播放出來是噪音,後來讀gsm源代碼,才發現wav文件存儲的字節高低位和普通我們認知的高低位是相反的。1個字節8位,按順序0-7位,我們平常認爲最左邊是0bit,最右邊是7bit,但wav存儲的相反,最左邊是最高位7bit,最右邊是最低位0bit。
要把顛倒了的順序重新顛倒過來,甚爲複雜,此處就不貼代碼了,詳細可參考gsm的開源代碼。