Win32下的录音编程

 

1 引言

  在Win32 APIs基础上编写录音程序繁琐易错,使用封装好的类是个不错的注意。不幸的是所谓封装好的类对你而言,往往是代码罗嗦且功能不足,因此尽管你可能希望在某个项目上因使用封装好的类而避开Win32 APIs,可最终你发现你还得面对它。不是为了编写自己的类,就是为了修改别人的代码。

  Win32 APIs中有一组被称成多媒体控制接口(即MCI)的函数,该接口提供了多媒体编程所需的系统级APIs。对绝大多数C/C++程序员而言,这些函数也就是Windows多媒体编程的最低层接口。

  由于录音代码直接操作真实的录音设备而非单纯的逻辑过程,因此会遇到一些“意外”或与时序有关的困难,从而使编写健壮的代码成了一件困难的事。

  录音的目的往往是将声音写入文件保存下来或是通过网络发送,这两类需求对录音代码影响较大。前者无实时性要求,一般也不限制数据量,代码较易编写,而后者既对实时性又要求,又对数据量(也就是音频格式)有要求,且网络系统的稳定性也难比文件系统。期望在本文中试图解决所有的问题是不现实的,诸如如何压缩音频数据以减小网络带宽需求或如何通过网络传输音频数据之类的问题,读者需参阅专业著作,本文只讲述如何获得适宜某类需求的原始音频数据。

  本文中的示例代码均是一个名为CWaveRecord的录音类的成员函数,该类是一个网络电话程序的核心类。为减小篇幅或简单明了,作了简化处理等。

  2 基本过程

  MCI按打开设备、配置设备、实现功能(或曰发送命令)、撤销配置、关闭设备的标准次序组织APIs。对于录音编程而言,其要点在于根据音频格式打开对应的设备、配置录音所需的参数(主要是设置数据区以及根据数据接收方式设置回调函数或消息)、按一定次序发送命令给设备、接收数据并配置参数以继续录音、停止录音释放资源、关闭设备等几个步骤上。所需的函数说明于mmsystem.h,引入库是winmm.lib

  2.1 打开设备

  调用waveInOpen()打开录音设备,打开成功后函数返回MMSYSERR_NOERROR,而第一个参数返回设备句柄。

  调用该函数时,必须指定设备表示符(可能有多个设备)、音频格式以及返回音频数据的方式。

  你可以将设备表示符,即第二个参数设为WAVE_MAPPER。这意味着,你不在意具体使用哪个设备,由系统决定。

  第三个参数是个WAVEFORMATEX结构指针,对应的结构指定音频格式。一个音频格式结构的例子:

  PCMWAVEFORMAT wf_PCM8S11K =

  {

   {

   WAVE_FORMAT_PCM, // PCM格式

   1, // 单声道

   11025L, // 11.025KHZ

   11025L, // 11025字节/s

   1 // 字节对齐

   },

   8 // 8位采样

  };

  后面的三个参数用于指定获取音频数据的方式:事件方式、线程方式、窗口方式以及回调函数方式。最后一个参数,即第六个参数指定方式,而第四个参数和第五个参数指定该方式所需的信息。

  调用示例:

  waveInOpen( &m_hWaveIn, dwDeviceID, m_pwf,

  (DWORD)waveInProc, (DWORD)this, CALLBACK_FUNCTION );

  该示例使用回调函数方式获取音频数据。其中dwDeviceID通常即为WAVE_MAPPERm_pwf指向音频格式结构,缺省值即wf_PCM8S11K的地址,waveInProc是回调函数(内部细节后面论述)。值得注意的是,this指针被设为实例数据,这是一个编程小技巧,方便waveInProc调用处理函数。

    关于第2个参数,有一点需要强调的是即使被指定为WAVE_MAPPER,即由操作系统自动选择相应的设备,也有可能会得到MMSYSERR_BADDEVICEID的结果,即设备ID出错。这有可能是因为操作系统中根本没有任何录音设备可供选择(这个时候如果调用waveInGetNumDevs函数,将会返回0)。

  2.2 配置设备

  按指定音频格式成功打开录音设备之后,需要配置录音设备,也即为音频数据分配数据区。需要考虑的第一个问题是数据块的个数和大小。

  系统一般在填满一个数据块后将其返回,这意味着大的数据块导致获取数据的频率降低。如果试图通过网络传输这些数据,这意味着传输次数降低,但每次传输的数据量较大,这将导致音频的实时性降低。另外,在网络传输过程中,数据包(指每次发送的UDPTCP数据包)的开始一般需设置一个信息头,纪录本数据包所含音频数据的长度、录制起始时间、录制时间、发送时间、识别用的标识符以及其他一些信息,方便接受者处理。如果设置的音频数据块太小,比如100字节,将导致低的网络传输效率。这意味着网络将为小量数据启动传输过程,而这小量数据中又有相当一部分属于附加信息,这是不合算的。

  数据块的个数相对来说要好处理的多。较多的数据块只有一个问题,那就是浪费,但这对Windows这样的操作系统而言,浪费几百K的内存根本不是问题。数据块个数不能太少,这是因为在你处理上次返回的数据块时,录音设备正在工作,需要内存。如果在某一段时间内,比如说网络传输变坏,你处理数据的速度也将变慢,而录音设备却如常工作。这导致你无法迅速地返回当前内存供下一次录音使用,因此需要一些备用的内存块以备不时之需。

  调用waveInAddBuffer为录音设备配置数据块,而在此之前需调用waveInPrepareHeader准备数据块。

  WaveInPrepareHeader的参数有三个,就是录音设备句柄、一个描述数据块的结构WAVEHDR和该结构的大小。

  WAVEHDR结构是录音编程的常用数据结构,不同的使用环境设置不同的参数,具体内容参见MSDN

  下面的示例为录音设备添加4个大小为4096字节的数据块:

   for( i=0; i<4; i++ )

   {

   pwh = (WAVEHDR*)malloc( 4096 + sizeof(WAVEHDR) )

   ZeroMemory( pwh, sizeof(WAVEHDR) );

   pwh->dwBufferLength = 4096;

   pwh->lpData = (LPSTR)(pwh + 1);

   waveInPrepareHeader( hWaveIn, pwh, sizeof(WAVEHDR) );

   waveInAddBuffer( hWaveIn, pwh, sizeof(WAVEHDR) );

  }

  每次添加数据块都需一个WAVEHDR(其他操作也如此),能否让这些数据块共用一个WAVEHDR或使用局部变量以减少内存消耗呢?最好不要这么做,因为你是在与实际设备打交道。很可能当调用返回时,系统内部却还在使用WAVEHDR结构。如果共用或使用局部变量将导致信息混乱或非法内存操作。这一点MSDN没有强调,我花了一星期才得到如此教训。其他录音操作也类似于此,切记!

  配置好内存以后就可启动录音了:

  waveInStart( hWaveIn );

  如果所有操作均正确,很快你将得到第一块音频数据。

  2.3 处理音频数据块

  获取音频数据块的方式有多种(详见MSDN),处理方式大同小异,本文以回调函数方式为例说明处理过程。如上所述,waveInProcwaveInOpen调用中被设置为回调函数。

  waveInProc通过处理WIM_OPEN(打开设备)、WIM_DATA(音频数据)、WIM_CLOSE(设备关闭)消息的方式与系统交互,通常只需处理WIM_DATA消息,其他两条消息可以忽略。

  当系统返回一个音频数据块时,系统调用回调函数。此时,回调函数第一个参数被设置为录音设备句柄;第二个参数是消息,即WIM_DATA;第三个参数则是在waveInOpen第五个参数中给出的实例数据;其他参数是与消息有关的参数,参见MSDN

   处理WIM_DATA的任务由成员函数OnWaveInData完成,因此waveInProc及其简单:

   CWaveRecord *pObject = (CWaveRecord *)dwInstance;

   if( uMsg == WIM_DATA )

   pObject->OnWaveInData( (WAVEHDR*)dwParam1 );

   在OnWaveInData中需要完成两件事:一是保存系统返回的音频数据,二是给录音设备添加数据块以继续录音,否则会因数据块耗尽而停止录音。

  在保存系统返回的音频数据块之前,需要调用waveInUnprepareHeader发送一个通知告诉设备驱动程序该数据块已不能用于录音。保存音频数据块是个单纯的数据保存问题,不需多说,只是有三个要点需要特别关注:

  一是MSDN中注明在回调函数中严格限制系统调用,因此你不能随心所欲地设计方案,比如说直接将音频数据写入文件是不允许的。通常,你需要设计一个数据块链表结构进行缓冲,这多半涉及多线程编程。特别是如果你使用MFC中的CSocket类,那么必须清楚CSocket依赖一个名为CSocketWnd的内部类,而该类与TLS(线程局部存贮)有关,这意味着CSocket类是不支持多线程共享的。然而,Win32SOCKET句柄是全局性。因此,获取CSocketWnd类对象的窗口句柄之后,我通过直接调用Win32 APIs使用CSocket::m_hSocket避开这个由MFC封装引起的问题。

  二是系统是在另一个线程(不是你自己创建的线程)中调用回调函数,这会给使用TLS机制的程序带来一些微妙的影响。如果你使用MFCAfxGetApp之类的函数返回值不正确。

  三是尽可能地快速。

  至于给录音设备添加数据块以继续录音,其步骤与初始化过程一样:

  waveInPrepareHeader( hWaveIn, pwh, sizeof(WAVEHDR));

  waveInAddBuffer( hWaveIn, pwh, sizeof(WAVEHDR) );

  2.4 关闭设备释放资源

  简单的调用waveInClose即可关闭设备。不过,在关闭时必须保证所有录音数据块已全部返回,这有点麻烦。我的解决方法是设置一个停止录音标志和一个录音数据块计数。当用户发出停止录音的命令后,该标志置为TRUE,而OnWaveInData(在回调函数中被调用)检查该标志,在标志置位的情况下,OnWaveInData不添加录音数据块。每添加一个录音数据块计数增加,每返回一个则减少(均在OnWaveInData中实现)。执行停止录音命令时,先将停止标志值位,等待计数变为零,然后才调用waveInClose。需要注意的是,计数涉及多线程数据共享,应使用线程互斥机制,在单CPU的机子上使用InterlockedIncrementInterlockedDecrement是个简单的选择。

  3 编程技巧

  3.1 简单的声音检测

  如果你通过网络发送音频数据,自然需要进行音频处理。专业级的处理难度相当高,如果你是在一个局域网上进行通话,那么无需进行音频压缩也可以将就使用(性能自然不高)。但如果你不说话,程序还不停地发送音频数据(静音或噪音),那就太说不过去了。

  你可以定义一个静音区间,一旦音频强度进入该区间就意味着没有声音。统计音频数据块中不是静音采样点的个数,当总数超过预设的限制时,才发送该数据块。

  对于8位采样,127是静音点。简单的代码如下:

   for( i=0; i<nMax; i++ )

   {

   uValue = (UCHAR)data[i];

   if( uValue<=125 || uValue>=129 )

   uHasCnt++;

   if( uHasCnt >= uLestNum )

   return TRUE;

   }

  return FALSE;

  3.2 创建音频文件

  创建任意格式的音频文件在编程上不是一件轻松的事,但对于8位单声道音频数据而言较容易。定义文件信息头结构:

  // wav文件头结构, 对齐方式为1字节

  typedef struct

  {

  char RiffId[4]; // "RIFF"

   DWORD dwFileDataSize; // file size - 8

   char WaveId[4]; // "WAVE"

   char FMTId[4]; // "fmt "

   DWORD dwFmtSize; // 16

   WORD wFormatTag; // WAVE_FORMAT_PCM

   WORD wChannels; // 1

   DWORD dwSamplesPerSec; // 11025

   DWORD dwAvgBytesPerSec; // 11025

   WORD wBlockAlign; // 1

   WORD wBitsPerSample; // 8

   char DataId[4]; // "data"

   DWORD dwDataSize;

  } WAVEFILEHDR, *PWAVEFILEHDR;

  打开文件时,预留该结构大小的空间:

  SetFilePointer(hFile,sizeof(WAVEFILEHDR), NULL, FILE_BEGIN );

  然后就如写一般文件一样将接收的音频数据写入文件:

   WriteFile(hFile,pwh->lpData,pwh->dwBytesRecorded,&n, NULL );

   m_dwSizeWrite += dwSize;

  最后一行代码是将写入的数据量统计下来用于信息头填写。

  在关闭音频文件时填写信息头:

   WAVEFILEHDR wfd =

   {

   ''R'', ''I'', ''F'', ''F'', 0,

   ''W'', ''A'', ''V'', ''E'',

   ''f'', ''m'', ''t'', '' '', 16,

   WAVE_FORMAT_PCM, 1, 11025, 11025, 1, 8,

   ''d'', ''a'', ''t'', ''a'', 0

   };

   wfd.dwFileDataSize = dwBytes + sizeof(WAVEFILEHDR)- 8;

   wfd.dwDataSize = dwBytes;

   ::SetFilePointer( hFile, 0, NULL, FILE_BEGIN );

   ::WriteFile( hFile, &wfd, sizeof(wfd), &dwSize, NULL );

  参考文献

  1 Visual C++ 6.0 MSDN

  2 Win32程序员开发指南(二)

  3 Visual C++ 6.0附带MFC源码

文章出处(有一点非常小的补充):DIY部落(http://www.diybl.com/course/3_program/c++/cppsl/2008222/100464.html)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值