GSM音频编码的优化和写入wav文件
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的开源代码。