波形音频(WAVE)底层接口的学习与使用

在WINDOWS下,音频函数有多种类型,如MCI、多媒体OLE控制、高级音频等,使用方法都比较简单。 但如果想编写一个功能较强大的音频处理程序,那就必须使用低级音频函数和多媒体文件I/O来控制音频设备的输入和输出。

因为低级音频函数可直接与音频驱动程序交互,通过窗口消息或回调(CALLBACK)函数来管理音频数据块的记录和播放,控制非常灵活。重要的一点是,低级音频函数为我们提供了一个设备无关的接口。

  Header: Declared in Mmsystem.h; include Windows.h.   Library: Use Winmm.lib.

WAVEOUT

1.waveOutGetNumDevs函数 The waveOutGetNumDevs function retrieves the number of waveform-audio output devices present in the system. 获取系统中波形音频输出设备的数目,如果为0,呵呵。

2.waveOutGetDevCaps The waveOutGetDevCaps function retrieves the capabilities of a given waveform-audio output device. 获取波形输出设备的性能、能力

//声明:
MMRESULT waveOutGetDevCaps(      //成功返回 0; 否则返回错误值
  UINT_PTR      uDeviceID,       //要查询的输出设备的ID
  LPWAVEOUTCAPS pwoc,            //WAVEOUTCAPS结构体指针,用于接受设备信息 
  UINT          cbwoc            //结构体大小
);
//可能的错误值
MMSYSERR_BADDEVICEID = 2; //设备ID超界
MMSYSERR_NODRIVER    = 6; //没有安装驱动程序

//WAVEOUTCAPS的定义:
typedef struct { 
    WORD      wMid;                  //制造商ID
    WORD      wPid;                  //产品ID
    MMVERSION vDriverVersion;        //版本号;高字节是主版本号,低字节是次版本号
    TCHAR     szPname[MAXPNAMELEN];  //产品名称
    DWORD     dwFormats;             //支持的格式
    WORD      wChannels;             //声道
    WORD      wReserved1;            //填充
    DWORD     dwSupport;             //其他功能支持
} WAVEOUTCAPS;

//dwFormats格式:Mono单声道,Stereo立体声
WAVE_INVALIDFORMAT = $00000000; //invalid format
WAVE_FORMAT_1M08   = $00000001; //11.025 kHz, Mono,   8-bit 
WAVE_FORMAT_1S08   = $00000002; //11.025 kHz, Stereo, 8-bit 
WAVE_FORMAT_1M16   = $00000004; //11.025 kHz, Mono,   16-bit
WAVE_FORMAT_1S16   = $00000008; //11.025 kHz, Stereo, 16-bit
WAVE_FORMAT_2M08   = $00000010; //22.05  kHz, Mono,   8-bit 
WAVE_FORMAT_2S08   = $00000020; //22.05  kHz, Stereo, 8-bit 
WAVE_FORMAT_2M16   = $00000040; //22.05  kHz, Mono,   16-bit
WAVE_FORMAT_2S16   = $00000080; //22.05  kHz, Stereo, 16-bit
WAVE_FORMAT_4M08   = $00000100; //44.1   kHz, Mono,   8-bit 
WAVE_FORMAT_4S08   = $00000200; //44.1   kHz, Stereo, 8-bit 
WAVE_FORMAT_4M16   = $00000400; //44.1   kHz, Mono,   16-bit
WAVE_FORMAT_4S16   = $00000800; //44.1   kHz, Stereo, 16-bit

//dwSupport:功能支持
WAVECAPS_PITCH          = $0001; //支持音调控制
WAVECAPS_PLAYBACKRATE   = $0002; //支持播放速度控制
WAVECAPS_VOLUME         = $0004; //支持音量控制
WAVECAPS_LRVOLUME       = $0008; //支持左右声道音量控制
WAVECAPS_SYNC           = $0010; //The driver is synchronous and will block while playing a buffer.设备同步但播放缓冲时阻塞
WAVECAPS_SAMPLEACCURATE = $0020; //Returns sample-accurate position information.返回精确样本信息
WAVECAPS_DIRECTSOUND    = $0040; 

3.waveOutOpen--MM_WOM_OPEN The waveOutOpen function opens the given waveform-audio output device for playback. 打开波形设备

//声明
MMRESULT waveOutOpen(
  LPHWAVEOUT     phwo,                //(返回)指向设备句柄的指针
  UINT_PTR       uDeviceID,           //将要被打开的波形音频输出装置的ID ,它可以是一个装置ID,也可以是一个已经打开的波形音频输入装置柄。
                                      //当有多种波形输出设备时,建议使用WAVE_MAPPER常数作为设备ID,这使waveOutOpen函数
                                      //会自动挑选最适合播放给定的数据格式的设备。
  LPWAVEFORMATEX pwfx,                //一个指向将被送到音频数据设备的WAVEFORMATEX结构的指针
  DWORD_PTR      dwCallback,          //它指向一个特定的CALLBACK函数,事件句柄,窗口句柄,
                                      //或一个将在波形音频回放时以便处理与回放进度相关的消息的期间呼叫的线程ID,如果无须CALLBACK函数,可以将其设为0 。
  DWORD_PTR      dwCallbackInstance,  //传递到CALLBACK进程的用户实例数据。如果是窗口CALLBACK进程的话,该参数不用(设为0)
  DWORD          fdwOpen              //用来打开装置的标识(FLAGS)
);

//fdwOpen:标示
CALLBACK_EVENT             //The dwCallback parameter is an event handle. dwCallback参数栏是事件句柄
CALLBACK_FUNCTION          //The dwCallback parameter is a callback procedure address. dwCallback 参数栏是CALLBACK函数地址
CALLBACK_NULL              //No callback mechanism. This is the default setting. 无CALLBACK进程,默认设置
CALLBACK_THREAD            //The dwCallback parameter is a thread identifier. dwCallback 参数栏是线程ID
CALLBACK_WINDOW            //The dwCallback parameter is a window handle. dwCallback 参数栏是窗口句柄
WAVE_ALLOWSYNC             //If this flag is specified, a synchronous waveform-audio device can be opened. 
                           //If this flag is not specified while opening a synchronous driver, the device will fail to open. 
                           //如果该项被设置,一个同步的装置能被打开。如果在打开一个同步驱动时没有用该项,装置打开将会失败。
WAVE_FORMAT_DIRECT         //If this flag is specified, the ACM driver does not perform conversions on the audio data.  
WAVE_FORMAT_QUERY          //If this flag is specified, waveOutOpen queries the device to determine if it supports the given format, 
                           //but the device is not actually opened.如果设定该项,waveOutOpen 询问装置来决定是否支持给定的格式,但装置实际上并没有被打开。 
WAVE_MAPPED                //If this flag is specified, the uDeviceID parameter specifies a waveform-audio device to be mapped to by the wave mapper. 
                           //该项被设定后uDeviceID参数表示一个被声波映射装置映射的波形装置。

4.waveOutPrepareHeader The waveOutPrepareHeader function prepares a waveform-audio data block for playback. 准备工作

//声明
MMRESULT waveOutPrepareHeader(
  HWAVEOUT hwo,      //设备句柄
  LPWAVEHDR pwh,     //WAVEHDR指针
  UINT cbwh          //WAVEHDR结构大小
);

//WAVEHDR:
typedef struct { 
    LPSTR      lpData;             //指向波形数据缓冲的指针
    DWORD      dwBufferLength;     //缓冲大小
    DWORD      dwBytesRecorded;    //波形头结构用于输入时,标识缓冲中数据的数量
    DWORD_PTR  dwUser;             //用户数据
    DWORD      dwFlags;            //标识缓冲区
    DWORD      dwLoops;            //循环次数
    struct wavehdr_tag * lpNext; 
    DWORD_PTR reserved; 
} WAVEHDR;

//dwFlags:缓冲区标识
WHDR_BEGINLOOP       //This buffer is the first buffer in a loop.  This flag is used only with output buffers.
WHDR_DONE            //Set by the device driver to indicate that it is finished with the buffer and is returning it to the application.
WHDR_ENDLOOP         //This buffer is the last buffer in a loop.  This flag is used only with output buffers.
WHDR_INQUEUE         //Set by Windows to indicate that the buffer is queued for playback.
WHDR_PREPARED        //Set by Windows to indicate that the buffer has been prepared with the waveInPrepareHeader or waveOutPrepareHeader function.

//清除WAVEHDR
MMRESULT waveOutUnprepareHeader(
  HWAVEOUT hwo,  
  LPWAVEHDR pwh, 
  UINT cbwh      
);

5.waveOutWrite--MM_WOM_DONE The waveOutWrite function sends a data block to the given waveform-audio output device. 发送数据到音频输出设备

//播放
MMRESULT waveOutWrite(
  HWAVEOUT hwo,  
  LPWAVEHDR pwh, 
  UINT cbwh      
);

通过使用waveOutPause、waveOutRestart、waveOutReset(MM_WOM_DONE)和waveOutClose(MM_WOM_CLOSE)来进行暂停、重新启动、停止播放和关闭设备。

WAVEIN

记录数字音频的方法基本同播放过程,不同在于记录期间是不提供诸如暂停和重新开始这样的控制的。 使用到的函数包括:waveInGetNumDevs、waveInGetDevCaps、waveInOpen(MM_WIM_OPEN)、waveInPrepareHeader、waveInUnPrepareHeader、waveInAddBuffer、waveInReset(MM_WIM_DATA)、waveInStart、waveInStop、waveInClose(MM_WIM_CLOSE)等。 音频的输入大体分三步:

1 打开设备 -----waveInOpen(打开一个音频输入设备) 2 开始录音------waveInStart开始录音 3 关闭设备------waveInClose关闭录音。之前调用一下waveInReset,这样可以清掉尚在等待录音的缓冲区

常用的相关API为: waveInOpen(打开一个音频输入设备) waveInPrepareHeader(为一个即将在waveInAddBuffer中调用的输入缓冲区准备头部) waveInAddBuffer(添加一个输入用的数据缓冲区) waveInStart(开始录音) waveInClose(关闭音频输入设备)等几个,以及需要在waveInOpen中指定的一个回调函数或者线程,其作用是在一个数据缓冲区被录满后被调用,以对这些数据进行处理,和其他一些相关的操作。注意这里的一个数据缓冲区。

//waveInOpen
MMRESULT waveInOpen( 
LPHWAVEIN phwi,             // phwi是返回的句柄存放地址
UINT uDeviceID,             // uDeviceID是要打开的音频设备ID号,一般都指定为WAVE_MAPPER
LPWAVEFORMATEX pwfx,
DWORD dwCallback,           // dwCallback则为指定的回调函数或线程,窗口等的地址
DWORD dwCallbackInstance,   // dwCallbackInstance为需要向回调函数或线程送入的用户参数
DWORD fdwOpen               // fdwOpen指定回调方式:CALLBACK_FUNCTION, CALLBACK_THREAD和CALLBACK_WINDOW
);

至于pwfx,则比较关键,它指定了要以什么音频格式打开音频输入设备,它是一个结构WAVEFORMATEX:

typedef struct { 
WORD wFormatTag;        //可以在wFormatTag中指定一些压缩的音频格式,如G723.1,TURE DSP,等之类。不过一般都是选用WAVEFORMAT_PCM格式,
                        //即未压缩的音频格式,至于压缩,可以在录完后调用下面将要谈到的ACM单独进行。
WORD nChannels;         //nChannels为声道数,1或者2。
DWORD nSamplesPerSec;   //nSamplesPerSec为每秒采样数,8000、11025、22050、44100为几个标准值。
DWORD nAvgBytesPerSec;  //每秒平均的字节数,在PCM方式中就等于nChannels*nSamplesPerSec*wBitsPerSample/8,
                        //但对于其它的压缩的音频格式,由于很多压缩方式是按时间片进行的,如G723.1,就是以30ms为一个压缩单位,
                        //这样,nAvgBytesPerSec只是一个大概的数字,并不准确,程序中的计算是不应该以这个量为准的。
                        //这一点在下面的压缩音频输出和ACM音频压缩中非常重要。
WORD nBlockAlign;       //nBlockAlign是一个比较特殊的值,表示对音频处理时的最小处理单位,对于PCM非压缩,它就是wBitsPerSample*nChannels/8,
                        //而对于非压缩格式,则表示压缩/解压处理的最小单位了,如G723.1,就是30ms的数据大小(20bytes或者24bytes)。
WORD wBitsPerSample;    //wBitsPerSample就是每采样值的位数,8或者16。
WORD cbSize;            //cbSize则是表示该WAVEFORMATEX的结构在标准的头部之后还有多少字节数,对于很多非PCM的音频格式,
                        //有一些自己的定义格式参数,这些就紧跟在标准的WAVEFORMATEX后面,其大小就由cbSize指定。对于PCM格式而言,为0,或者忽略不检查。
} WAVEFORMATEX;

下面要做的事情就是准备几个用做录音的缓冲区。常准备多个缓冲区,并在回调中循环使用。对于缓冲区,得使用waveInPerpareHeader准备一下头部,这个API比较简单,如果你是循环使用缓冲区,对每个缓冲区也只需要调用一次waveInPrepareHeader。为什么要使用一次就可以。参看waveInPerpareHeader说明就明白。此函数功能就是定位缓冲区的数据区地址,和数据大小。以便为系统所用。 A)首先得确定一下需要用什么回调方式,即在某个时间片的音频数据被录完后,Windows将通过这个回调来激活对这些数据的处理过程,一般用到的无非是FUNCTION、THREAD和EVENT这几类,而比较方便简单的就是FUNCTION和THREAD了。FUNCTION方式是指Windows会调用你这个函数,而THREAD则是由Windows来激活你所指定的线程。这些都在waveInOpen中指定。    B)一切准备好之后,就可以调用waveInAddBuffer和waveInStart开始录音了,只要你一调用这个waveInStart,录音就开始了,即使这个缓冲区录满之后你没有加入新的缓冲区进去,录音也不会停,只是这中间的语音数据全都丢了。当通过waveInAddBuffer送入的缓冲区被录满后,Windows就会通过你在waveInOpen中指定的方式进行回调,在回调中把录好的语音数据取出来,并且,如果还想继续录音的话,得将下一个缓冲区添加进去。考虑到这个处理是有时间延迟的,而且音频对时间很敏感,一般都要先预加入若干个缓冲区,有人提出:比如,一共定义了8个缓冲区,而为了保险起见,最好保证任一时刻至少有3个缓冲区可被录音使用,那么在开始录音时,则先加入4个缓冲区,然后在回调中,如果当前录好的缓冲区第n个,则对第(n+4)%8调用waveInAddBuffer,这时,还有第(n+1)%8,(n+2)%8, (n+3)%8这三个缓冲区可用,即基本上就可以保证所录得音频中不会有断开的间隔。比如0,1,2,3这些个先加入,当0好的时候对4,5 ,6 ,7调用waveInAddBuffer。   如此这样何不:开始的时候把8个全部放入缓冲区,当一个缓冲区满后调用回调,处理后立即把这个缓冲区重用,继续添加到缓冲区队列中。不更简单明了。如下   mmReturn = ::waveInPrepareHeader ( m_hRecord, pHdr, sizeof(WAVEHDR) ); //准备   mmReturn = ::waveInAddBuffer ( m_hRecord, pHdr, sizeof(WAVEHDR) );//添加   注意这两步都是在回调,或者线程中处理的。   C)想结束录音时,最好在waveInClose之前调用一下waveInReset,这样可以清掉尚在等待录音的缓冲区,这里常见的问题是等待的缓冲区清理了,可是正在用的缓冲区怎么办?如果这个时候就用waveInClose,那么系统会出错。解决方法一:在回调函数中注意,一个缓冲区满后,不要再用waveInAddBuffer增加缓存,当缓冲区用到1的时候调用waveInReset清掉尚在等待录音的缓冲区继续waveInClose。   总结上面的注意3点:回调的选取,注意缓冲区的原理,注意结束的处理

windows waveform方式实现录音要通过这么几步:(注意顺序!!) 一、打开录音设备 waveinopen()函数 注意,调用之前要填写好wav头信息(包含采样率、采样位数等);还要定义好回调函数等,回调函数的解释后面讲。 二、准备好录音缓存空间 waveinprepareheader()函数 这一步为了准备好将要送入录音设备的缓存,以便之后可以供之使用。 一般至少需要准备两块缓存。因为录音不能间断,当一块填满时没有时间等待你去送入下一块缓存,所以必须提前就准备好。 三、将缓存送入录音设备 waveinaddbuffer()函数 将缓存送入录音设备,供之存入已录下的音频。开始录音时,应至少送入两块不同的缓存,即调用两次这个函数。之后为了不致录音产生间断,应保证至少有一块缓存在录音时为空,以备衔接。 四、开始录音 waveinstart()函数 当以上的工作都准备好时,便可调用此函数开始录音了。一旦调用,录音设备便立即录音并存入已经送来的缓存块内,当被送来的有多个缓存块时,按照FIFO的原则向缓存块内存入录音数据。此函数执行之后可以执行一个while()循环,来等待录音设备录音。为了减少cpu使用率,可以在循环中加人sleepex(x,TRUE),x单位ms,TRUE必须要有。 每个缓冲块存满时,会产生一个回调信号,并调用回调函数(或回调窗口等,具体定义在waveinopen函数内,这里只讲回调函数的情况);回调信号自动被回调函数接收,回调函数根据回调信号来执行各种相应的操作。回调函数执行完后,程序跳回到原来执行位置继续执行。回调函数的具体如下: 回调信号一般有三个,对应着三种回调函数被调用的情况: 1、  WIM_OPEN 当执行waveinopen()函数时,会调用回调函数,并产生这个回调信号。代表录音设备已经打开。在这次回调函数的调用中,可以自己设定一些操作,也可以没有操作。   2、  WIM_DATA 当每块缓存块填满时,产生这个回调信号,并调用回调函数。在这次调用中,回调函数应当完成这样的工作,以便录音连续进行:         将存满的缓存块处理,例如存入文件,或送入其他设备;         向录音设备送入新的缓存块;录音设备任何时刻应当拥有不少于2个的缓存块,以保证录音不间断性。 3、  WIM_CLOSE 当调用waveinclose函数时,会产生这个回调信号,代表录音设备关闭成功。这次回调函数调用中,可以执行相应的一些关闭文件保存信息等等的操作,自定义。 五、停止录音,关闭设备 waveinstop()  停止录音 waveinreset() 复位 waveinclose()关闭设备     依次调用这些函数,来结束录音。 最后,注意在代码开头要包含windows.h和mmsystem.h两个头文件,还要加人库winmm.lib,用#pragma comment(lib,”winmm.lib”)即可。 主要顺序就是这些,注意每一步要完成的工作,一旦没有按顺序执行或者没有把每步应当完成的工作做完,录音是不能够启动的。

以下是各函数返回值的部分可能结果:

    /* general error return values */  //MMSYSERR_BASE == 0  
    #define MMSYSERR_NOERROR        0                    /* no error */  
    #define MMSYSERR_ERROR          (MMSYSERR_BASE + 1)  /* unspecified error */  
    #define MMSYSERR_BADDEVICEID    (MMSYSERR_BASE + 2)  /* device ID out of range */  
    #define MMSYSERR_NOTENABLED     (MMSYSERR_BASE + 3)  /* driver failed enable */  
    #define MMSYSERR_ALLOCATED      (MMSYSERR_BASE + 4)  /* device already allocated */  
    #define MMSYSERR_INVALHANDLE    (MMSYSERR_BASE + 5)  /* device handle is invalid */  
    #define MMSYSERR_NODRIVER       (MMSYSERR_BASE + 6)  /* no device driver present */  
    #define MMSYSERR_NOMEM          (MMSYSERR_BASE + 7)  /* memory allocation error */  
    #define MMSYSERR_NOTSUPPORTED   (MMSYSERR_BASE + 8)  /* function isn't supported */  
    #define MMSYSERR_BADERRNUM      (MMSYSERR_BASE + 9)  /* error value out of range */  
    #define MMSYSERR_INVALFLAG      (MMSYSERR_BASE + 10) /* invalid flag passed */  
    #define MMSYSERR_INVALPARAM     (MMSYSERR_BASE + 11) /* invalid parameter passed */  
    #define MMSYSERR_HANDLEBUSY     (MMSYSERR_BASE + 12) /* handle being used */  
                                                         /* simultaneously on another */  
                                                         /* thread (eg callback) */  
    #define MMSYSERR_INVALIDALIAS   (MMSYSERR_BASE + 13) /* specified alias not found */  
    #define MMSYSERR_BADDB          (MMSYSERR_BASE + 14) /* bad registry database */  
    #define MMSYSERR_KEYNOTFOUND    (MMSYSERR_BASE + 15) /* registry key not found */  
    #define MMSYSERR_READERROR      (MMSYSERR_BASE + 16) /* registry read error */  
    #define MMSYSERR_WRITEERROR     (MMSYSERR_BASE + 17) /* registry write error */  
    #define MMSYSERR_DELETEERRO     (MMSYSERR_BASE + 18) /* registry delete error */  
    #define MMSYSERR_VALNOTFOUND    (MMSYSERR_BASE + 19) /* registry value not found */  
    #define MMSYSERR_NODRIVERCB     (MMSYSERR_BASE + 20) /* driver does not call DriverCallback */  
    #define MMSYSERR_MOREDATA       (MMSYSERR_BASE + 21) /* more data to be returned */  
    #define MMSYSERR_LASTERROR      (MMSYSERR_BASE + 21) /* last error in range */  
      
    /* waveform audio error return values */ //WAVERR_BASE == 32  
    #define WAVERR_BADFORMAT       (WAVERR_BASE + 0)     /* unsupported wave format */  
    #define WAVERR_STILLPLAYING    (WAVERR_BASE + 1)     /* still something playing */  
    #define WAVERR_UNPREPARED      (WAVERR_BASE + 2)     /* header not prepared */  
    #define WAVERR_SYNC            (WAVERR_BASE + 3)     /* device is synchronous */  
    #define WAVERR_LASTERROR       (WAVERR_BASE + 3)     /* last error in range */  

    详解WAVE音频文件格式     WAVE声音文件格式是目前Windows最直接保存声音数据的文件格式.在涉及声音信号处理时大多是对WAV文件直接操作,有必要搞清楚所研究声音的文件格式. RIFF文件与WAV文件     在Windows环境下,大部分多媒体文件都依循着一种结构来存放信息,称为资源互换文件格式(Resources Interchange File Format),简称RIFF。比如声音的WAV文件,视频的AVI文件,动画的MMM文件等均是由此结构衍生出来的.所以,要掌握多媒体文件格式,首先得认识RIFF的结构.     RIFF是一种树状结构,其基本组成单位是chunk(即块),每个chunk由辨识码,数据大小和数据组成,如下图。可以看出,一个chunk的长度,就是数据的大小加上8Byte.

一般而言,chunk本身不允许内部再包含chunk,但有两个例外:以"RIFF"和以"UST"为辨识码的chunk。针对这两种chunk,RIFF又从原先的"裸数据"中切出4Byte作为"格式辨别码",如下图所示.

对RIFF的树状结构有所了解之后,可以知道它相当于一个根目录,而格式辨识码则相当于具体的盘符如C:,D:等等.Windows下的各种多媒体文件格式就如同在磁盘机下规定只能存放怎样的目录,而在该目录下仅能存放何种数据.

WAV文件头

顾名思义,WAV就是波形音频文件(Wave Audio),是Windows中用来表示数字化声音的一种标准格式,其文件扩展名为.wav,是一种非常简单的RIFF文件,格式辨识码为"WAVE".整个WAV文件分成两部分:文件头和数据块.WAV格式文件主要有两种文件头.

标准的44字节文件头

这种WAV是最简单的一种RIFF格式,包含两个chunk:<fmt—chunk>,<wave—data>,这两个子块都是一个WAV文件必须包含的.

RIFF WAVE Chunk

以'RIFF'作为标示,然后紧跟着为size字段,该size是整个wav文件大小减去ID和Size所占用的字节数,即FileLen - 8 =Size.然后是Type字段,为'WAVE',表示是wav文件.结构定义如下:

struct RIFF_HEADER
{
	char szRiffID[4];  // 'R','I','F','F'
	DWORD dwRiffSize;
	char szRiffFormat[4]; // 'W','A','V','E'
};

Format Chunk

以'fmt '作为标示.一般情况下Size为16,此时最后附加信息没有;如果为18则最后多了2个字节的附加信息.主要由一些软件制成的wav格式中含有该2个字节的

struct WAVE_FORMAT
{
	WORD wFormatTag;
	WORD wChannels;
	DWORD dwSamplesPerSec;
	DWORD dwAvgBytesPerSec;
	WORD wBlockAlign;
	WORD wBitsPerSample;
};
struct FMT_BLOCK
{
	char  szFmtID[4]; // 'f','m','t',' '
	DWORD  dwFmtSize;
	WAVE_FORMAT wavFormat;
}; 

为了产生出能够正确读出的WAV文件,必须严格注意以下几个分量间的特定关系,否则产生出的文件将无法正常播放: 58字节文件头如果不是Windows的标准WAV文件,而是经过了一些软件处理的,往往就是58字节的文件头,如下图所示.

它比44字节的多了一个fact子块.<fact—ck>储存了关于WAV文件内容的重要信息.该子块定义如下:

Fact Chunk

Fact Chunk是可选字段,一般当wav文件由某些软件转化而成,则包含该Chunk.结构定义如下:

struct FACT_BLOCK
{
      char  szFactID[4]; // 'f','a','c','t'
      DWORD  dwFactSize;
};

"data"子块数据安排方式

"data"子块中装的是真正的声音数据.除非安装其它特殊软件,否则Windows目前仅提供WAVE_FORMAT_PCM一种数据格式,即脉冲编码调制(Pulse Code Modulation).针对此惭式,Windows中"data"子块中数据存放的形式如下图所示,根据声道数不同及取样位数的不同,安排4位的位置.

Data Chunk头结构定义如下:

struct DATA_BLOCK
 {
       char szDataID[4]; // 'd','a','t','a'
       DWORD dwDataSize;
 };

注意:Windows中将16位值的范围定为[-32768,32767].另外,0并不一定代表无声,而是由中间数值来决定,即8位的时候为128,l6位时0才是无声.所以,编程中需要放入无声的数据时,必须先认清声音格式是l6位还是8位. 通常解压缩后得到的文件仅仅是裸数据,不能正常播放声音.了解了WAV文件格式后,就可以按照标准的44字节格式,在解码数据前编写一个正确的WAV文件头,使其成为一个有效的WAV文件.

下面是我实现的录音类GWaveIn:

头文件:

#pragma once

#include <Windows.h>
#include <MMSystem.h>
using namespace std;

class GWaveIn
{
public:
	GWaveIn(void);
	virtual ~GWaveIn(void);

	bool IfWaveIn();                //检查设备
	bool PrepareWaveIn(HWND hwnd, BYTE* pbuf1, BYTE* pbuf2);           //录音的准备工作
	bool AddBuffer(PWAVEHDR pwavehdr);
	bool StartRec();
	bool CloseWaveIn();

public:
	HWAVEIN    m_hWaveIn;
	UINT_PTR   m_iWaveInID;
	WAVEHDR   m_head1;
	WAVEHDR   m_head2;
};

CPP文件:

#include "StdAfx.h"
#include "GWaveIn.h"

GWaveIn::GWaveIn(void)
{
}

GWaveIn::~GWaveIn(void)
{
}

bool GWaveIn::IfWaveIn()
{
	MMRESULT mmresult = 0;

	mmresult = waveInGetNumDevs();
	if(mmresult == 0) return false;

	WAVEINCAPS waveincaps = {0};
	unsigned int i = 0;
	unsigned int num = mmresult;
	for( ; i<num; i++)
	{
		mmresult = waveInGetDevCaps(i,&waveincaps,sizeof(WAVEINCAPS));
		if(mmresult != MMSYSERR_NOERROR) 
			return false;
		if((waveincaps.dwFormats & WAVE_FORMAT_1M08) == 0) 
			continue;
		else 
		{
			m_iWaveInID = i;
			return true;
		}
	}

	return false;
}

bool GWaveIn::PrepareWaveIn(HWND hwnd, BYTE* pbuf1, BYTE* pbuf2)
{
	WAVEFORMATEX waveformatex = {0};
	waveformatex.wFormatTag = WAVE_FORMAT_PCM;
	waveformatex.nChannels = 1;
	waveformatex.nSamplesPerSec = 11025;
	waveformatex.nAvgBytesPerSec = 11025*1*8/8;
	waveformatex.nBlockAlign = 8*1/8;
	waveformatex.wBitsPerSample = 8;
	waveformatex.cbSize = 0;

	MMRESULT mmresult = 0;
	mmresult = waveInOpen(&m_hWaveIn,m_iWaveInID,&waveformatex,(DWORD)hwnd,0,CALLBACK_WINDOW);
	if(mmresult != MMSYSERR_NOERROR) return false;

	
	m_head1.lpData = (LPSTR)pbuf1;
	m_head1.dwBufferLength = 20480;
	m_head1.dwBytesRecorded = 0;
	m_head1.dwUser = 0;
	m_head1.dwFlags = 0;
	m_head1.dwLoops = 1;
	m_head1.lpNext = NULL;
	m_head1.reserved = 0;

	mmresult = waveInPrepareHeader(m_hWaveIn,&m_head1,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	m_head2.lpData = (LPSTR)pbuf2;
	m_head2.dwBufferLength = 20480;
	m_head2.dwBytesRecorded = 0;
	m_head2.dwUser = 0;
	m_head2.dwFlags = 0;
	m_head2.dwLoops = 1;
	m_head2.lpNext = NULL;
	m_head2.reserved = 0;

	mmresult = waveInPrepareHeader(m_hWaveIn,&m_head2,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	return true;
}

bool GWaveIn::AddBuffer(PWAVEHDR pwavehdr)
{
	MMRESULT mmresult = 0;
	mmresult = waveInAddBuffer(m_hWaveIn,pwavehdr,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	return true;
}

bool GWaveIn::StartRec()
{
	MMRESULT mmresult = 0;
	mmresult = waveInStart(m_hWaveIn);
	if(mmresult != MMSYSERR_NOERROR) return false;

	return true;
}

bool GWaveIn::CloseWaveIn()
{
	MMRESULT mmresult = 0;
	waveInStop(m_hWaveIn);
	waveInReset(m_hWaveIn);
	waveInClose(m_hWaveIn);

	if(m_head1.lpData != NULL) free(m_head1.lpData);
	waveInUnprepareHeader(m_hWaveIn,&m_head1,sizeof(WAVEHDR));

	if(m_head2.lpData != NULL) free(m_head2.lpData);
	waveInUnprepareHeader(m_hWaveIn,&m_head2,sizeof(WAVEHDR));

	return true;
}

播放类GWaveOut:

头文件:

#pragma once

#include <windows.h>
#include <MMSystem.h>
using namespace std;

class GWaveOut
{
public:
	GWaveOut(void);
	virtual ~GWaveOut(void);

	bool IfWaveOut();
	bool PrepareWaveOut(HWND hwnd, BYTE* pbuf1, BYTE* pbuf2);
	bool WaveOutWrite(PWAVEHDR pwavehdr);
	bool CloseWaveOut();

public:
	HWAVEOUT m_hWaveOut;
	UINT_PTR m_iWaveOutID;
	WAVEHDR m_head1;
	WAVEHDR m_head2;
};

CPP文件:

#include "StdAfx.h"
#include "GWaveOut.h"

GWaveOut::GWaveOut(void)
{
}

GWaveOut::~GWaveOut(void)
{
}

bool GWaveOut::IfWaveOut()
{
	MMRESULT mmresult = 0;
    mmresult = waveOutGetNumDevs();
	if(mmresult == 0) return false;

	WAVEOUTCAPS waveoutcaps = {0};
	unsigned int i = 0;
	unsigned int num = mmresult;
	for( ; i<num; i++)
	{
		mmresult = waveOutGetDevCaps(i,&waveoutcaps,sizeof(WAVEOUTCAPS));
		if(mmresult != MMSYSERR_NOERROR) return false;
		if((waveoutcaps.dwFormats & WAVE_FORMAT_1M08) == 0)
			continue;
		else
		{
			m_iWaveOutID = i;
			return true;
		}
	}

	return false;
}

bool GWaveOut::PrepareWaveOut(HWND hwnd, BYTE* pbuf1, BYTE* pbuf2)
{
	WAVEFORMATEX waveformatex = {0};
	waveformatex.wFormatTag = WAVE_FORMAT_PCM;
	waveformatex.nChannels = 1;
	waveformatex.cbSize = 0;
	waveformatex.nAvgBytesPerSec = 11025*1*8/8;
	waveformatex.nBlockAlign = 1;
	waveformatex.nSamplesPerSec = 11025;
	waveformatex.wBitsPerSample = 8;

	MMRESULT mmresult = 0;
	mmresult = waveOutOpen(&m_hWaveOut,m_iWaveOutID,&waveformatex,(DWORD)hwnd,0,CALLBACK_WINDOW);
	if(mmresult != MMSYSERR_NOERROR) return false;

	m_head1.lpData = (LPSTR)pbuf1;
	m_head1.dwBufferLength = 20480;
	m_head1.dwBytesRecorded = 0;
	m_head1.dwUser = 0;
	m_head1.dwFlags = 0;
	m_head1.dwLoops = 1;
	m_head1.lpNext = NULL;
	m_head1.reserved = 0;

	mmresult = waveOutPrepareHeader(m_hWaveOut,&m_head1,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	m_head2.lpData = (LPSTR)pbuf2;
	m_head2.dwBufferLength = 20480;
	m_head2.dwBytesRecorded = 0;
	m_head2.dwUser = 0;
	m_head2.dwFlags = 0;
	m_head2.dwLoops = 1;
	m_head2.lpNext = NULL;
	m_head2.reserved = 0;

	mmresult = waveOutPrepareHeader(m_hWaveOut,&m_head2,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	return true;
}

bool GWaveOut::WaveOutWrite(PWAVEHDR pwavehdr)
{
	MMRESULT mmresult = 0;
	waveOutWrite(m_hWaveOut,pwavehdr,sizeof(WAVEHDR));
	if(mmresult != MMSYSERR_NOERROR) return false;

	return true;
}

bool GWaveOut::CloseWaveOut()
{
	waveOutReset(m_hWaveOut);//停止声音处理并产生MM_WOM_DONE消息,关键
	waveOutClose(m_hWaveOut);

	if(m_head1.lpData != NULL) free(m_head1.lpData);
	waveOutUnprepareHeader(m_hWaveOut,&m_head1,sizeof(WAVEHDR));

	if(m_head2.lpData != NULL) free(m_head2.lpData);
	waveOutUnprepareHeader(m_hWaveOut,&m_head2,sizeof(WAVEHDR));

	return true;
}

其实,掌握了音频的录制和播放的基本原理之后,难道就在于对内存的管理了,要考虑的问题包括内存不足、内存生存期的分配、内存浪费。。。等等。

最后附上我实现的工程:http://download.csdn.net/detail/gongluck93/9681666

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏恰同学骚年

.NET Core微服务之服务间的调用方式(REST and RPC)

  微服务之间的接口调用通常包含两个部分,序列化和通信协议。常见的序列化协议包括json、xml、hession、protobuf、thrift、text、by...

1086
来自专栏分布式系统进阶

Librdkafka对Kafka Metadata的封装和操作

int rd_kafka_metadata_cache_wait_change (rd_kafka_t *rk, int timeout_ms) { int ...

861
来自专栏进击的程序猿

MapReduce浅读MapReduce概要

几个小时要处理完TB的数据,但是这些程序一般都不是分布式系统人员开发的,使用起来因为一些分布式的系统问题,会非常的痛苦

792
来自专栏谦谦君子修罗刀

程序员面试闪充 -- 性能优化

CPU 和GPU 关于绘图和动画有两种处理方式CPU(中央处理器)和GPU(图形处理器),CPU的工作都在软件层面,而GPU的在硬件层面。 总的来说,可以使用...

35813
来自专栏刘望舒

Android系统启动流程(二)解析Zygote进程

前言 上一篇文章我们分析了init进程,init进程中主要做了三件事,其中一件就是创建了Zygote进程,那么Zygote进程是什么,它做了哪些事呢?这篇文章会...

1958
来自专栏Seebug漏洞平台

以太坊网络架构解析

区块链的火热程度一直以直线上升,其中以区块链 2.0 —— 以太坊为代表,不断的为传统行业带来革新,同时也推动区块链技术发展。

2572
来自专栏张善友的专栏

使用C# 和Consul进行分布式系统协调

随着大数据时代的到来,分布式是解决大数据问题的一个主要手段,随着越来越多的分布式的服务,如何在分布式的系统中对这些服务做协调变成了一个很棘手的问题。今天我们就来...

3095
来自专栏后端技术探索

PHP非阻塞模式

让PHP不再阻塞当PHP作为后端处理需要完成一些长时间处理,为了快速响应页面请求,不作结果返回判断的情况下,可以有如下措施:

621
来自专栏圣杰的专栏

ABP入门系列(13)——Redis缓存用起来

源码路径:Github-LearningMpaAbp 1. 引言 创建任务时我们需要指定分配给谁,Demo中我们使用一个下拉列表用来显示当前系统的所有用户,以...

2329
来自专栏杨建荣的学习笔记

缓慢的update语句性能分析(r6笔记第61天)

最近处理一个问题的时候,先是收到DB time升高的报警,然后查看DB time的情况发现,已经有近1000%的负载了。 ? 带着好奇心想看看到底是什么样的...

2645

扫码关注云+社区