音频和视频是多媒体应用程序向用户提供信息的主要方式,这些音频、视频数据一般都具有较高的采样率,经过压缩的原始数据才具有实用价值,否则不仅要占用大量存储空间而且在播放或进行网络传输时效率也是非常低下的,所以音频、视频数字压缩编码在多媒体应用中有着广泛而又重要的用途。本文主要对音频的编码压缩作了阐述。
音频的编码压缩方式有许多种,如基于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; //继续枚举 } |
typedef struct { HACMDRIVERID hadid; WORD wFormatTag; } FIND_DRIVER_INFO; |
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); //关闭驱动器 …… } |
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; |
WORD wFormatTag = WAVE_FORMAT_DSPGROUP_TRUESPEECH; HACMDRIVERID hadid = find_driver(wFormatTag); |
WAVEFORMATEX* pwfDrv = get_driver_format(hadid, wFormatTag); // 获得格式的详情 |
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); |
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); |
mmr = acmStreamOpen(&hstr, had, //驱动器句柄 pwfPCM, //源格式 pwfDrv, //目标格式 NULL, //无过滤 NULL, //无回调 0, //实例化数据 ACM_STREAMOPENF_NONREALTIME); |
DWORD dwDst2Bytes = pwfDrv->nAvgBytesPerSec * dwDst1Samples / pwfPCM->nSamplesPerSec; dwDst2Bytes = dwDst2Bytes * 3 / 2; unsigned char * pDst2Data = new unsigned char [dwDst2Bytes]; |
double result= (double) dwSrcBytes / (double) strhdr2.cbDstLengthUsed; |