使用CODECs压缩Wave音频
概要
微软的Win95和WinNT操作系统都包含有能够压缩解压缩Wave音频流的CODECs。将你的wave 音频以压缩形式保存不但能够减少对存储空间的需求,在网络上传送时也能减少数据传输 的时间。
本文及其附带的实例代码告诉你怎样使用安装在Windows系统中的CODECs来压缩和解压缩音 频。稍稍改变这些代码就可以用作解压缩经过压缩的数据,执行数据格式转换。 所附实例代码是用Microsoft Visual C++ 5.0版本开发的,并在Win95和WinNT 4.0操作系 统上测试过。
简介
Win95及最近的WinNT都具有能过安装的CODECs处理压缩的wave格式的音频和视频数据流的 能力。
一个CODEC是一小段用于压缩(COmpress)及解压缩(DECompress)数据流的代码(因此, 得名CO-DEC)。许多CODECs即能压缩又能解压缩。而一些CODECs仅能用于解压缩,这样私 有数据可以在系统上播放,但数据格式不能在系统上创建。
尽管一个CODEC原则上能够用于压缩解压缩任一种数据流,还是设计有各种各样的CODECs 以实现以高的压缩比率,更好的保真度或实时压缩性能来压缩某种数据类型。例如,获取 高的视频压缩数据压缩率的最好方法应用于音频数据时未必就能得到相同的效果,反之也 然。
本文着重于怎样在自己的代码中使用CODEC将音频数据以你的系统中CODECs所支持的方式进 行压缩。压缩音频数据的一个主要原因是降低存储某一声音序列所需数据量。少的数据量 意味着声音所占有的空间更少,并且能够以更快的速度在MODEM和网络上传递。如果数据 以Windows系统所支持的某一通用格式压缩的话,则可以不必手工解压缩就直接播放--系 统将使用它自己的CODECs解压缩数据并播放。
我的系统中有什么CODECs?
Win95和WinNT本身附带有几种标准的CODECs,也可由系统中所安装的应用程序安装其他的 CODECs。例如,DSP Group,Inc. TrueSpeech CODEC随Win95发送,因此你写的任何应用于 Win95的程序都可应用此CODEC(假如用户没有在控制面板中删除它或禁止它的话)。以后 可能要安装的CODEC的一个例子是微软网络(MSN)软件自已所用的音频数据。
所有安装的CODECs由音频压缩管理器(ACM)管理。我们可以从一小程序中查询ACM来查到安 装了哪些CODECs,它们都支持什么格式。你也可双击控制面板中的多媒体选项,选择高级 标签,就能看到系统中所安装的CODECs。
介绍应用ACM,得知它所管理每一个CODEC都可以做些什么的一个好方法是写一个简单的 查询ACM的命令行应用程序。本文所附带的CAPS程序完成的就是这个功能--让我们看看它的 代码,我将给你一起分析此程序,解释每一步完成的什么功能。
首先从调用ACM编程接口所需的包含的头文件开始 :
#include <windows.h>
#include <mmsystem.h>
#include <mmreg.h> // 多媒体注册
#include <msacm.h> // 音频压缩管理器
#include <stdio.h>
mmsystem.h头文件定了Windows支持大部分的多媒体功能,但不包含ACM接口及任何厂商定义。 mmreg.h包含了对不同厂商设计的各种wave数据类型的格式标签的定义。它也包含了用于处理 不同的wave数据类型的结构( 基于WAVEFORAMTEX)的定义。msacm.h包含了ACM所需的API, 标志等等。
我们要做的第一件事就是执行一些常见的ACM查询来判断版本号,获取诸如它当前管理了多少 个驱动程序的的信息。下面是查询ACM的部分代码:
// 取得ACM版本号
DWORD dwACMVer = acmGetVersion();
printf("ACM version %u.%.02u build %u",
HIWORD(dwACMVer) >> 8,
HIWORD(dwACMVer) & 0x00FF,
LOWORD(dwACMVer));
if (LOWORD(dwACMVer) == 0) printf(" (Retail)");
printf("/n");
// 显示一些ACM的信息
printf("ACM metrics:/n");
DWORD dwCodecs = 0;
MMRESULT mmr = acmMetrics(NULL, ACM_METRIC_COUNT_CODECS, &dwCodecs);
if (mmr) {
show_error(mmr);
}
else {
printf("%lu codecs installed/n", dwCodecs);
}
CAPS实例查询了更多的ACM信息。你可以仔细查看它的代码,运行程序得知结果。
对ACM有了简单了解后,现在可以要求它枚举出系统中当前所有的驱动程序。我们在程序中所 调用的枚举函数使用回调函数来汇报每个设备的数据,这在Windows编程是一种很普遍的方法。 下面的调用就是枚举当前ACM所管理的所有设备:
// 枚举所有允许的驱动程序
printf("Enabled drivers:/n");
mmr = acmDriverEnum(DriverEnumProc, 0, 0);
if (mmr) show_error(mmr);
如同其它多媒体函数,许多ACM函数调用返回一MMRESULT值,指出了可能发生的错误。此值为0 表示函数成功执行。现在,让我们看看枚举回调函数DriverEnumProc,它由系统中的每一个驱 动程序调用:
BOOL CALLBACK DriverEnumProc(HACMDRIVERID hadid, DWORD dwInstance, DWORD fdwSupport)
{
printf(" id: %8.8lxH", hadid);
printf(" supports:/n");
if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_ASYNC) printf(" async conversions/n");
if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CODEC) printf(" different format conversions/n");
if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_CONVERTER) printf(" same format conversions/n");
if (fdwSupport & ACMDRIVERDETAILS_SUPPORTF_FILTER) printf(" filtering/n");
// 获得一些具体信息
ACMDRIVERDETAILS dd;
dd.cbStruct = sizeof(dd);
MMRESULT mmr = acmDriverDetails(hadid, &dd, 0);
if (mmr) {
printf(" "); show_error(mmr);
}
else {
printf(" Short name: %s/n", dd.szShortName);
printf(" Long name: %s/n", dd.szLongName);
printf(" Copyright: %s/n", dd.szCopyright);
printf(" Licensing: %s/n", dd.szLicensing);
printf(" Features: %s/n", dd.szFeatures);
printf(" Supports %u formats/n", dd.cFormatTags);
printf(" Supports %u filter formats/n", dd.cFilterTags);
}
// 打开驱动程序
HACMDRIVER had = NULL;
mmr = acmDriverOpen(&had, hadid, 0);
if (mmr) {
printf(" "); show_error(mmr);
}
else {
DWORD dwSize = 0;
mmr = acmMetrics(had, ACM_METRIC_MAX_SIZE_FORMAT, &dwSize);
if (dwSize cbSize = LOWORD(dwSize) - sizeof(WAVEFORMATEX);
pwf->wFormatTag = WAVE_FORMAT_UNKNOWN;
ACMFORMATDETAILS fd;
memset(&fd, 0, sizeof(fd));
fd.cbStruct = sizeof(fd);
fd.pwfx = pwf;
fd.cbwfx = dwSize;
fd.dwFormatTag = WAVE_FORMAT_UNKNOWN;
mmr = acmFormatEnum(had, &fd, FormatEnumProc, 0, 0);
if (mmr) {
printf(" ");
show_error(mmr);
}
free(pwf);
acmDriverClose(had, 0);
}
return TRUE; // 继续枚举
}
驱动程序向回调函数传递了描述驱动程序所支持类型的一组标志。一些驱动程序可以异步 操作,而另一些驱动程序则不能。一些驱动程序能够将一种wave数据格式转换成另一种格 式(称作CODECs),而另一些驱动程序仅能完成过滤操作,其输入输出格式是一样的。注 意ACM维护着这类数据及驱动程序的名字,版权信息等等,这样我们可以不必装载或打开 指定的驱动程序就可以得到这些数据。这样很方便,譬如当需将数据放在列表框中由用户 选择时。
要获得有关某一驱动程序能力更多的详细信息,必须装载驱动程序并打开它,可通过调用 acmOpenDriver实现。一旦驱动程序打开,可请求枚举它所支持的wave数据格式。同时有一 个小问题--尽管所有wave格式描述结构基于WAVEFORAMTEX,许多格式使用此结构的扩展形 式来保存其特定的信息。如果我们想枚举所有格式,需要知道为此结构分配多少供驱动 程序填写详细信息的空间。可通过向acmMetrics函数传递ACM_METRIC_MAX_SIZE_FORMAT标 志得到所需的最大的结构的尺寸。
如果你读过上面的代码,你会发现我只是简单的将分配的空间强制转换为WAVEFORMATEX指针。 我只对通用信息而不是任一特定类型的数据感兴趣,因此这个指针符合我的要求。
为结构分配了空间后,现在我可以acmFormatEnum来枚举所支持的格式。这次又用到一个回 调函数来取得枚举出的格式的相关数据:
BOOL CALLBACK FormatEnumProc(HACMDRIVERID hadid, LPACMFORMATDETAILS pafd,
DWORD dwInstance, DWORD fdwSupport)
{
printf(" %4.4lXH, %s/n", pafd->dwFormatTag, pafd->szFormat);
return TRUE; // 继续枚举
}
如你所看到的,这是一次尝试并仅打印出格式的某些信息。
这样,通过上面的代码,你能够查询ACM所有的驱动程序,查找每一个驱动程序所支持的 格式。我建议你现在运行CAPS程序,看看你的系统上安装了些什么。
使用特定的CODEC
好了,我们已得知你的系统上安装了什么CODECs--现在来看看怎样查找某一特定的CODEC并 使用它压缩音频数据。让我们看看CONV实例,它使用一种有效的CODEC压缩一个简单的wave 数据包。为了使代码更趋简单,我以控制台应用程序的形式的实现它,也没有尝试去 播放压缩过的数据或将其存入文件。这个实例的代码仅向你展示怎样找到你所需要的驱 动程序并使用它将数据转换为压缩格式。剩下的就靠你了。
两步实现压缩
在理想的情况下,压缩一些数据可能只不过是向系统说:“这有一些数据,请压缩成这种格式。” 不幸的是,Windows编程与理想相去甚远,象通常一样,我们得自已做许多琐碎的工作。 要解决的第一个也是最重要的问题是给定的CODEC可能不能压缩你所使用的数据格式。例 如,我们录入了一些11025Hz,8位,单声道的PCM数据(或许是用户向麦克风说的话),此 种格式几乎所有的多媒体PC都能录制。我们可能要将数据通过MODEM传递,因此我们想尽可能 的压缩数据,使数据量减少。我们选择了TrucSpeech CODEC,它安装在Windows中,能够 获得大约10:1的压缩率。我们所要碰到的问题就是TrueSpeech CODEC不能处理11025Hz, 8位,单声道的PCM数据。它只能处理8000Hz,16位,单声道的数据(某些情况下是8位)。 因此我们必须先将源数据转换为TrueSpeech CODEC所支持的中间PCM格式,然后在使用它 将中间数据转换为最终所需的格式。
可使用随Windows分发的某种不同的CODEC将一种PCM格式转换为另一种格式,因此你需使用 某种CODEC将数据转换为其它CODEC能够处理的格式。我们已知道怎样去枚举CODECs及其所 支持的格式,因此这样做是可以实现的。
但还有一个问题,我在实例代码中忽略了,留给你们解决。如果某一CODEC能够创建我们所 想要的压缩格式,但支持几种不同的输入格式,我们怎样选择最佳的中间格式呢?按照 Nigel的准则,那就是“总是做最少量的工作”,我选择使用CODEC所支持的枚举出的第一 种PCM格式。由于很容易实现,可能会导致数据失真。假设我们要使用的某一CODEC有一些 近乎无失真的压缩算法,能够接收8位或16位的11025Hz或22050Hz的PCM数据。我们要转换 以441000Hz,16位立体声录制的高保真的样本。我们试图降低数据量,而不在乎损失质量。 如果我们枚举此CODEC所支持的格式,第一个得到的可能是11025Hz,8位单声道的格式。我 们先将数据转换为此格式,然后进行压缩,这其间肯定要损失一些质量,因为这种中间格 式不够好。如果使用16位22050Hz的话会好一些。已告诉过你这种缺憾啦,让我们瞧瞧CONV 实例,看它是怎样工作的。
CONV实例程序
CONV实例分四个阶段:它创建一些wave格式数据的样本,找到一个合适的CODEC,将数据转 换为此CODEC可处理的中间格式,最后将数据转换成所需的格式。为了简单其间,源数据用 程序创建而不是录入或是从wave文件中读取:
// 首先我们创建一个可能是刚刚才录制的wave 其格式为11.025kHz,
// 8位单声道PCM,这是一个所有机器上都可用的录入格式,我们的例
// 子是1秒长的1kHz的正弦波wave,刚好1000个周期
WAVEFORMATEX wfSrc;
memset(&wfSrc, 0, sizeof(wfSrc));
wfSrc.cbSize = 0;
wfSrc.wFormatTag = WAVE_FORMAT_PCM; // PCM
wfSrc.nChannels = 1; // Mono
wfSrc.nSamplesPerSec = 11025; // 11.025 kHz
wfSrc.wBitsPerSample = 8; // 8 bit
wfSrc.nBlockAlign = wfSrc.nChannels * wfSrc.wBitsPerSample / 8;
wfSrc.nAvgBytesPerSec = wfSrc.nSamplesPerSec * wfSrc.nBlockAlign;
DWORD dwSrcSamples = wfSrc.nSamplesPerSec;
BYTE* pSrcData = new BYTE [dwSrcSamples]; // 1秒种的长度
BYTE* pData = pSrcData;
double f = 1000.0;
double pi = 4.0 * atan(1.0);
double w = 2.0 * pi * f;
for (DWORD dw = 0; dw /pre>
上面的代码创建了一个WAVEFORMATEX结构用来描述源数据格式,并用简单的数学方法生成了1
秒钟长的11.025kHz,8位单声道的PCM的wave数据。
下一步就是选择要将数据转换成什么格式及选定一个合适的CODEC。
WORD wFormatTag = WAVE_FORMAT_DSPGROUP_TRUESPEECH;
// 现在我们选定一个支持目标格式标签的CODEC
HACMDRIVERID hadid = find_driver(wFormatTag);
if (hadid == NULL) {
printf("No driver found/n");
exit(1);
}
printf("Driver found (hadid: %4.4lXH)/n", hadid);
find_driver函数枚举所有的驱动程序直到找到一个支持给定标签值的驱动程序(本例为
WAVE_FORMAT_DSPGROUP_TRUESPEECH)。我没有在此给出代码是因为它与前面的枚举代
码非常相象。随后你可以查看它是怎样工作的。
选定了驱动程序,现在要为最终驱动程序将产生的压缩数据格式创建一个WAVEFORMATEX
结构,并为驱动程序用于输入的中间PCM格式产生一个WAVEFORMATEX结构。
// 获得格式的详情
// 注意:这只是一个给定格式签的第一种或是最可能的格式
WAVEFORMATEX* pwfDrv = get_driver_format(hadid, wFormatTag);
if (pwfDrv == NULL) {
printf("Error getting format info/n");
exit(1);
}
printf("Driver format: %u bits, %lu samples per second/n",
pwfDrv->wBitsPerSample, pwfDrv->nSamplesPerSec);
// 获取驱动程序所支持的PCM格式标签
// 注意:我们只是选取第一支持的PCM格式但不一定是最好的选择
WAVEFORMATEX* pwfPCM = get_driver_format(hadid, WAVE_FORMAT_PCM);
if (pwfPCM == NULL) {
printf("Error getting PCM format info/n");
exit(1);
}
printf("PCM format: %u bits, %lu samples per second/n",
pwfPCM->wBitsPerSample, pwfPCM->nSamplesPerSec);
有点重复了,要注意的是get_driver_format函数仅仅枚举出第一种匹配的格式--也许不能获得
可能的最好的质量。
现在我们有了WAVEFORMATEX结构描述源格式,中间PCM格式,以及最终的压缩格式。可以开始转
换数据了。转换由被ACM称作流的对象来实现。我们可以打开流,将源格式、目标格式传递给它,
要求它进行转换。
在此要注意如果CODEC的算法复杂的话同步转换是很耗时的。一些CODEC可以异步工作,通过向
窗口发送一个消息,或调用一个回调函数,或设置一个事件告知你转换进程。下面的代码是以
最少麻烦准则完成任务的--你必须等待它直到完成。还有另外一点,很重要。如你所知,我们打
开转换流,指明ACM_STREAMOPENF_NONREALTIME标志。这非常重要。若省略此标志,那么一些
驱动程序(例如TrueSpeech驱动程序)将会报告错误512(意思是不可能)。此错误告诉你所要
求的转换不能实时进行。我的实例代码中没有这个问题,但如果你试图在播放数据的同时转换
大量数据,就要注意这点了。
让我们看看第一步的转换,它完成的是将源数据转换为中间格式:
/
// 将源wave转换为CODEC所支持的PCM格式
// 我们使用任一种能实现PCM格式间转换驱动程序
HACMSTREAM hstr = NULL;
mmr = acmStreamOpen(&hstr,
NULL, // 任一驱动程序
&wfSrc, // 源格式
pwfPCM, // 目标格式
NULL, // 无过滤
NULL, // 没回调
0, // 实例数据(未使用)
ACM_STREAMOPENF_NONREALTIME); // 标志
if (mmr) {
printf("Failed to open a stream to do PCM to PCM conversion/n");
exit(1);
}
// 为转换结果开辟一个缓冲区
DWORD dwSrcBytes = dwSrcSamples * wfSrc.wBitsPerSample / 8;
DWORD dwDst1Samples = dwSrcSamples * pwfPCM->nSamplesPerSec / wfSrc.nSamplesPerSec;
DWORD dwDst1Bytes = dwDst1Samples * pwfPCM->wBitsPerSample / 8;
BYTE* pDst1Data = new BYTE [dwDst1Bytes]; // 填写转换信息
ACMSTREAMHEADER strhdr;
memset(&strhdr, 0, sizeof(strhdr));
strhdr.cbStruct = sizeof(strhdr);
strhdr.pbSrc = pSrcData; // 要转换的源数据
strhdr.cbSrcLength = dwSrcBytes;
strhdr.pbDst = pDst1Data;
strhdr.cbDstLength = dwDst1Bytes; // 准备好头
mmr = acmStreamPrepareHeader(hstr, &strhdr, 0); // 转换数据
printf("Converting to intermediate PCM format.../n");
mmr = acmStreamConvert(hstr, &strhdr, 0);
if (mmr) {
printf("Failed to do PCM to PCM conversion/n");
exit(1);
}
printf("Converted OK/n");
// 关闭流
acmStreamClose(hstr, 0);
当流打开时,第二个参数为NULL,表示接受任何驱动程序进行转换。复杂的只是计算需要给输出
数据分配多大的缓冲区。由于PCM格式间的转换不牵扯压缩和解压缩,直接就计算出来了。
你可能注意到调用了acmStreamPrepareHeader,它为驱动程序安排好一切并允许驱动程序在转换前
锁定内存。
最后一步是将中间格式转换为最终的压缩格式:
///
// 将中间格式转换为最终的压缩格式
// 打开驱动程序
HACMDRIVER had = NULL;
mmr = acmDriverOpen(&had, hadid, 0);
if (mmr) {
printf("Failed to open driver/n");
exit(1);
}
// 打开转换流
// 注意使用了ACM_STREAMOPENF_NONREALTIME标志.
// 没有此标志一些软件压缩程序会报告512号错误--即不可能
mmr = acmStreamOpen(&hstr,
had, // 驱动程序句柄
pwfPCM, // 源格式
pwfDrv, // 目标格式
NULL, // 不过滤
NULL, // 无回调函数
0, // 实例数据(未使用)
ACM_STREAMOPENF_NONREALTIME); // 标志
if (mmr) {
printf("Failed to open a stream to do PCM to driver format conversion/n");
exit(1);
}
// 为转换结果分配一个缓冲区
// 根据以字节计的平均速率计算输出缓冲区的尺寸
// 并加上一机动位(bit)
// 没有此额外的空间IMA_ADPCM驱动程序将不能转换
DWORD dwDst2Bytes = pwfDrv->nAvgBytesPerSec * dwDst1Samples /
pwfPCM->nSamplesPerSec;
dwDst2Bytes = dwDst2Bytes * 3 / 2; // 增加一点空间
BYTE* pDst2Data = new BYTE [dwDst2Bytes]; // 填写转换信息
ACMSTREAMHEADER strhdr2;
memset(&strhdr2, 0, sizeof(strhdr2));
strhdr2.cbStruct = sizeof(strhdr2);
strhdr2.pbSrc = pDst1Data; // 要转换的源数据
strhdr2.cbSrcLength = dwDst1Bytes;
strhdr2.pbDst = pDst2Data;
strhdr2.cbDstLength = dwDst2Bytes; // 准备头
mmr = acmStreamPrepareHeader(hstr, &strhdr2, 0); // 转换数据
printf("Converting to final format.../n");
mmr = acmStreamConvert(hstr, &strhdr2, 0);
if (mmr) {
printf("Failed to do PCM to driver format conversion/n");
exit(1);
}
printf("Converted OK/n");
// 关闭流及驱动程序
mmr = acmStreamClose(hstr, 0);
mmr = acmDriverClose(had, 0);
以上与PCM格式间转换很相似,但此次我们提供了打开流时想要使用的驱动程序的句柄。实际上,
此处仍可使用NULL,因为已预知此驱动程序存在,但提供句柄避免了系统浪费时间为我们查找
此驱动程序。
计算用于压缩数据的缓冲区的尺寸有点难办,需要凭猜测。WAVEFORMATEX结构的nAvgBytesPerSec 域表示回放期间读取字节的平均速率。我们可使用它来估计存储压缩的wave需要多大空间。一 些驱动程序给出的数据确实是平均的,而不是最差场合下的值,因此我选择多增加50%的空间。 此方法在实践中虽然有些浪费但很有效。一旦转换完成,ACMSTREAMHEADER的结构的cbDstLengthUsed 域指出缓冲区实际用了多少字节。我使用它来计算压缩比:
// 显示转换统计结果
printf("Source wave had %lu bytes/n", dwSrcBytes);
printf("Converted wave has %lu bytes/n", strhdr2.cbDstLengthUsed);
printf("Compression ratio is %f/n", (double) dwSrcBytes /
(double) strhdr2.cbDstLengthUsed);
总结
使用Windows操作系统中所附的CODECs来压缩wave格式数据易于使用,并使数据占用存储空间更少,
传输时间更短。如果你有自已的压缩格式,可创建并安装自已的CODEC,象这里的代码一样使用它。