【 tulaoshi.com - 编程语言 】
摘要:本文介绍了在Microsoft Visual C++ 6.0下如何调用ACM(Audio Compression Manager,音频 压缩治理器)音频压缩编程接口对原始录入的Wave音频进行数据压缩。
要害字:ACM、CODECs、Wave音频、音频编码压缩
引言
!-- frame contents -- !-- /frame contents --
音频和视频是多媒体应用程序向用户提供信息的主要方式,这些音频、视频数据一般都具有较高的采样率,经过压缩的原始数据才具有实用价值,否则不仅要占用大量存储空间而且在播放或进行网络传输时效率也是非常低下的,所以音频、视频数字压缩编码在多媒体应用中有着广泛而又重要的用途。本文主要对音频的编码压缩作了阐述。
音频的编码压缩方式有许多种,如基于ITU-T G.728语音编码协议的LD-CELP 低时延码激励线性猜测编码、基于ITU-T G.711语音编码协议的PCM(Pulse Code Modulation ,脉冲编码调制)编码以及我们非常熟悉的GSM数字蜂窝移动电话的语音编码标准等等。这些不同的压缩方式有着不同的数据压缩比和还原音质,具体的编码格式和算法更是大相径庭。多数协议都比较复杂,普通程序难以实现其加、解压算法,而为多媒体提供了较强支持的Windows 98操作系统引入了ACM和VCM技术,用来治理系统中存在的所有的音频和视频编、解码器(Coder-Decoder,即CODECs,用来实现音频、视频数据编解码的驱动程序)。可以通过它们提供的编程接口调用系统中存在的现成的编解码器来实现音频数据的加、解压。Windows 98系统自带的音频CODECs 支持一些早期的音频数据压缩标准,如ADPCM (Adaptive Differential Pulse Code Modulation,自适应差分脉冲编码调制)编码等,而Internet EXPlorer 5.0 等应用程序包含的音频CODECs支持一些较新 的压缩标准, 如MPEG Layer 3等。本文所要介 绍的就是ACM音频压缩接口的编程方法,所使用的编程工具为Microsoft Visual C++ 6.0。
实现思路
尽管一个CODEC在理论上能够用于压缩、解压缩任一种数据流,但还是设计有各种各样的CODECs 以实现更高的压缩比、更高的保真度或实时压缩性能来压缩某种特定的数据类型。例如,把获取很高的视频压缩数据压缩率的最好方法应用到音频数据时未必就能得到相同的效果。
压缩音频数据的主要原理是降低存储某一声音序列所需的数据量。少的数据量就意味着声音所占有的空间更少,就能够以更快的速度通过MODEM在网络上传递。假如数据以Windows系统所支持的某种通用格式压缩的话,就可不经手工解压缩而直接播放--系 统将使用它自己的CODECs解压缩数据并播放。Windows 98本身附带有几种标准的CODECs,如DSP Group,Inc. TrueSpeech CODEC等。因此我们写的任何应用于 Windows 98下的程序都可应用这些CODEC,具体系统中都存在有哪些CODECs可以在控制面版的"多媒体"选项的"设备"标签页中查到。
CODEC 支持从源音频格式到目标格式的转换,而在实际应用中, 可能某种CODEC 不支持直接将源音频格式转换成目标格式,比如我们通过麦克风向多媒体计算机录入了一些频率为11025Hz、8位数据、单声道的PCM数据,假如选用系统的TrueSpeech CODEC进行处理,就会引起失败,因为这种CODEC只能处理频率为8KHz,16位单声道的数据。所以转换时要采取两步转换法,即先将源格式转换成一种中间格式,再将此中间格式转换成目标格式,因为线性PCM 编码 最为简单,且为绝大多数CODEC 所支持,所以一般中间格式都选为线性PCM 格式的一种。比如就可以先将原始数据转换成TrueSpeech CODEC所支持的中间PCM格式,然后再将其通过TrueSpeech CODEC转换成最终的压缩格式。
程序的设计实现
有关ACM的API函数定义在头文件msacm.h中, 除了在工程中加入对此头文件的引用之外, 对ACM编程还必须包含头文件mmsystem.h和mmreg.h,这两个头文件定义了多媒体编程中最基本 的常量和数据结构。为了避免有些高 版 本ACM才提供的函数和功能在较低版本的ACM中上不可用,程序中应调用acmGetVersion函 数查询用户机器中ACM 的版本信息。
虽然可以根据控制面版手工得到关于某种音频CODECs的信息,但在应用程序中也经常需要知道某种音频CODECs是否存在,并获取其编解码参数等信息,可以通过回调函数find_format_enum来枚举系统中的音频压缩格式:
BOOL CALLBACK find_format_enum(HACMDRIVERID hadid, LPACMFORMATDETAILS pafd, DWord dwInstance, DWORD fdwSupport)
{
FIND_DRIVER_INFO* pdi = (FIND_DRIVER_INFO*) dwInstance;
if (pafd-dwFormatTag == (DWORD)pdi-wFormatTag) {
pdi-hadid = hadid;
return FALSE; //停止枚举
}
return TRUE; //继续枚举
}
在该回调函数中用到的FIND_DRIVER_INFO是自定义的数据结构,其两个成员变量分别用来保存ACM驱动器号的句柄和要转换的数据格式:
typedef strUCt {
HACMDRIVERID hadid;
WORD wFormatTag;
} FIND_DRIVER_INFO;
现在可以枚举出系统中当前所有的驱动程序。我们在程序中所调用的枚举函数使用回调函数来汇报每个设备的数据,这在Windows编程是一种很普遍的方法。要获得有关某一驱动程序能力更多的具体信息,必须装载驱动程序并打开它,可通过调用 acmOpenDriver实现。一旦驱动程序打开,可请求枚举它所支持的wave数据格式。但这就存在一个问题:所有wave格式描述结构都基于WAVEFORAMTEX,许多格式使用此结构的扩展形式来保存其特定的信息。假如我们想枚举所有格式,需要知道为此结构分配多少供驱动 程序填写具体信息的空间。可以通过向acmMetrics函数传递ACM_METRIC_MAX_SIZE_FORMAT标 志得到所需的最大的结构的尺寸。打开驱动程序后要通过acmMetrics函数枚举到所支持的格式,该函数可以获取到许多ACM对象的有用信息。实现该过程的主要代码如下:
BOOL CALLBACK find_driver_enum (HACMDRIVERID hadid, DWORD dwInstance, DWORD fdwSupport)
{
……
MMRESULT mmr = acmDriverOpen(&had, hadid, 0);
//枚举所支持的格式
……
mmr = acmMetrics((HACMOBJ)had, ACM_METRIC_MAX_SIZE_FORMAT, &dwSize);
if (dwSize sizeof(WAVEFORMATEX)) dwSize = sizeof(WAVEFORMATEX);
WAVEFORMATEX* pwf = (WAVEFORMATEX*) malloc(dwSize);
……
pwf-cbSize = LOWORD(dwSize) - sizeof(WAVEFORMATEX);
pwf-wFormatTag = pdi-wFormatTag;
ACMFORMATDETAILS fd;
……
fd.cbStruct = sizeof(fd);
fd.pwfx = pwf;
fd.cbwfx = dwSize;
fd.dwFormatTag = pdi-wFormatTag;
mmr=acmFormatEnum(had, &fd, find_format_enum, (DWORD)(VOID*)pdi, 0); //枚举格式
……
acmDriverClose(had, 0); //关闭驱动器
……
}
根据指定的格式要找到其所对应的ACM驱动器号可以用枚举所有音频CODECs的ACM API函数acmDriverEnum来实现,在acmDriverEnum() 的参数中指定了在前面描述过的回调函数find_driver_enum,可以 进 一 步查询每个CODEC的信息,最终可以获取到ACM驱动器号的句柄。实现此功能的回调函数名为find_driver,本文后面将会用到。
在把原始Wave音频数据转换到中间PCM格式数据之前,需要做些前期预备工作,填充一些相关的结构信息,具体有:WAVEFORMATEX结构描述源格式、中间PCM格式、以及最终的压缩格式等。下面先填充一个用来描述源数据格式的WAVEFORMATEX结构:
WAVEFORMATEX wfSrc;
memset(&wfSrc, 0, sizeof(wfSrc));
wfSrc.cbSize = 0;
wfSrc.wFormatTag = WAVE_FORMAT_PCM; //PCM脉冲编码调制
wfSrc.nChannels = 1; //单声道
wfSrc.nSamplesPerSec = 11025; //11.025kHz
wfSrc.wBitsPerSample = 8; //8 bit
wfSrc.nBlockAlign = wfSrc.nChannels * wfSrc.wBitsPerSample / 8;
wfSrc.nAvgBytesPerSec = wfSrc.nSamplesPerSec * wfSrc.nBlockAlign;
然后通过前面提到的回调函数find_driver来获取由wFormatTag指定的中间数据格式所对应的驱动程序的ACM驱动器号,在此设定的是由WAVE_FORMAT_DSPGROUP_TRUESPEECH指定的有Windows 98系统自带的TrueSpeech CODEC:
WORD wFormatTag = WAVE_FORMAT_DSPGROUP_TRUESPEECH;
HACMDRIVERID hadid = find_driver(wFormatTag);
选定了驱动程序,现在要为最终驱动程序将产生的压缩数据格式创建一个WAVEFORMATEX结构,并为驱动程序用于输入的中间PCM格式产生一个WAVEFORMATEX结构:
WAVEFORMATEX* pwfDrv = get_driver_format(hadid, wFormatTag); // 获得格式的详情
在结构pwfDrv的成员变量wBitsPerSample里存放着驱动格式的位数,在nSamplesPerSec里存放着驱动格式的采样率。然后可以用非常类似的方法获取驱动程序所支持的PCM格式标签:
WAVEFORMATEX* pwfPCM = get_driver_format(hadid, WAVE_FORMAT_PCM);
当以上所需信息都以获取到后就可以开始转换数据了。转换由被ACM称作流的对象来实现。我们可以打开流,将源格式、目标格式传递给它,要求它进行转换。先将其转换成中间PCM格式。
将Wave音频转换为CODEC所支持的PCM格式
通过CODEC将源Wave音频转换成CODEC所支持的PCM格式,可以使用任何可以做PCM间转换的驱动器。另外还有一点很重要:我们打开转换流时,要指明ACM_STREAMOPENF_NONREALTIME标志。若省略此标志,那么一些驱动程序(例如TrueSpeech CODEC)将会报告发生第512号"不可能发生的"错误。该错误指明所要求的转换不能实时进行,假如在试图播放数据的同时转换大量数据,就必须注重这点。下面是该步转换过程的简要描述:
mmr = acmStreamOpen(&hstr,
NULL, //任意驱动器
&wfSrc, //源格式
pwfPCM, //目标格式
NULL, //无过滤
NULL, //无回调
0, //初始数据
ACM_STREAMOPENF_NONREALTIME);
根据以字节计的平均速率计算出输出缓冲区的大小,并加上一机动位(bit)假如没有此额外的空间IMA_ADPCM驱动程序将不能转换。中间的转换结果将存放在pDst1Data中:
DWORD dwSrcBytes = dwSrcSamples * wfSrc.wBitsPerSample / 8;
DWORD dwDst1Samples = dwSrcSamples * pwfPCM-nSamplesPerSec / wfSrc.nSamplesPerSec;
DWORD dwDst1Bytes = dwDst1Samples * pwfPCM-wBitsPerSample / 8;
unsigned char * pDst1Data = new unsigned char[dwDst1Bytes];
……
ACMSTREAMHEADER strhdr; //填充转换信息
memset(&strhdr, 0, sizeof(strhdr));
strhdr.cbStruct = sizeof(strhdr);
strhdr.pbSrc = cpBuf; //指定要转换的源Wave音频数据为cpBuf中的数据
strhdr.cbSrcLength = dwSrcBytes;
strhdr.pbDst = pDst1Data;
strhdr.cbDstLength = dwDst1Bytes;
mmr = acmStreamPrepareHeader(hstr, &strhdr, 0);
mmr = acmStreamConvert(hstr, &strhdr, 0); //转换数据
……
acmStreamClose(hstr, 0);
当流打开时,第二个参数为NULL,表示接受任何驱动程序进行转换。复杂的只是计算需要给输出数据分配多大的缓冲区。PCM格式间的转换不牵扯压缩和解压缩,缓冲区大小直接就计算出来了。至于调用acmStreamPrepareHeader这个ACM API函数,是由于它可以为驱动程序安排好一切并答应驱动程序在转换前锁定内存。
生成最终的压缩格式
向最终压缩格式的转换过程与前面的PCM格式间转换非常相似,只不过此次转换我们提供了打开流时想要使用的驱动程序的句柄。实际上,此处仍可使用NULL,因为已预知此驱动程序存在,但提供句柄避免了系统浪费时间为我们查找此驱动程序:
mmr = acmStreamOpen(&hstr,
had, //驱动器句柄
pwfPCM, //源格式
pwfDrv, //目标格式
NULL, //无过滤
NULL, //无回调
0, //实例化数据
ACM_STREAMOPENF_NONREALTIME);
另外,计算用于压缩数据的缓冲区的尺寸有点难办,需要凭猜测。WAVEFORMATEX结构的nAvgBytesPerSec 域表示回放期间读取字节的平均速率。我们可使用它来估计存储压缩的wave需要多大空间。一 些驱动程序给出的数据确实是平均的,而不是最差场合下的值,因此我选择多增加50%的空间。 此方法在实践中虽然有些浪费但很有效:
DWORD dwDst2Bytes = pwfDrv-nAvgBytesPerSec * dwDst1Samples / pwfPCM-nSamplesPerSec;
dwDst2Bytes = dwDst2Bytes * 3 / 2;
unsigned char * pDst2Data = new unsigned char [dwDst2Bytes];
其中,无符号字符型指针pDst2Data用于存储压缩的最终Wave音频数据,其大小经上式估算后存到dwDst2Bytes中。一旦转换完成,ACMSTREAMHEADER的结构的cbDstLengthUsed 域指出缓冲区实际用了多少字节。可以通过它来计算出压缩比:
double result= (double) dwSrcBytes / (double) strhdr2.cbDstLengthUsed;
当源信号为8K采样、16bits PCM编码、单声道、长度为1秒的Wave音频信号, 驱动程序采用Windows 98自带的TrueSpeech 音频CODEC,它能实现大约10:1的压缩,这样高的压缩率还是比较另人满足的。
小结:
本文以TrueSpeech CODEC为例对使用ACM音频压缩编程接口实现Wave音频压缩的过程作了介绍。假如有自已的压缩格式,也可创建并安装自已的CODEC,实现的方法与之基本类似。在理解了上述编程思想的前提下,对代码稍加改动就可编写出相 应的解压程序。本程序在windows 2000 Professional下,由Microsoft Visual C++ 6.0编译通过。