天天看点

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的开源代码。

继续阅读