上一篇中主要介绍了MCF中声音文件的播放。这一篇根据自己的理解介绍一下录音的实现,最后以实例的形式在Win32 APP中实现 录音并回放。下一篇将介绍一下录音在MFC中的实现,并在MFC中实现一个简单的录音机应用程序。
在介绍录音实现过程之前,先介绍一下数字音频基础知识。
1、数字音频基础知识
Fourier级数: 任何周期的波形可以分解成多个正弦波,这些正弦波的频率都是整数倍。级数中其他正线波的频率是基础频率的整数倍。基础频率称为一级谐波。
PCM: pulse code modulation,脉冲编码调制,即对波形按照固定周期频率采样。为了保证采样后数据质量,采样频率必须是样本声音最高频率的两倍,这就是Nyquist频率。
样本大小:采样后用于存储振幅级的位数,实际就是脉冲编码的阶梯数,位数越大表明精度越高,这一点参考数字逻辑电路。
声音强度: 波形振幅的平方。两个声音强度上的差常以分贝(db)为单位来度量,
计算公式如下: 20*log(A1/A2)分贝。A1,A2为两个声音的振幅。如果采样大小为8位,则采样的动态范围为20*log(256)分贝=48db。如果样本大小为16位,则采样动态范围为20*log(65536)大约是96分贝,接近了人听觉极限和痛苦极限,是再现音乐的理想范围。windows同时支持8位和16位的采样大小。
另外,常见的声音文件主要有两种,分别对应于单声道和双声道。如下表所示:
表1 声音相关参数对照表
音频数据格式 |
声道数 |
采样率KHz |
每秒钟的数据量(Byte) |
块对齐 Byte |
采样值Bit |
附加额外格式信息大小(Byte) |
单声道 |
1 |
11.025 |
11025*8*1/8=11025B=10.7K |
1 |
8 |
0 |
双声道 |
2 |
44.1 |
44100*16*2/8=176400B=172K |
4 |
16 |
0 |
声道数是指音频信道数,单声道的数据使用一个通道,立体声数据使用两个通道。对于PCM音频,这个值不能大于2;
采样率是指声音信号在“模→数”转换过程中单位时间内采样的次数。常取值8.0KHz,11.025 KHz,22.05 KHz,44.1 KHz。对于PCM音频,这个值不能大于44.1 KHz。采样率越高,丢失的信息相对越少,声音信号保存相对越完整,但是录音生成的文件也就越大;反之,采样率越低,声音信号保存相对不如采样率高时完整,但是录音生成的文件反而越小。系统提供了两种声道供用户选择,可综合对声音信号的要求高低和系统存储空间等因素灵活选择。
块对齐的字节数,块对齐是最低的数据的wFormatTag格式类型的原子单位。如果wFormatTag WAVE_FORMAT_PCM或WAVE_FORMAT_EXTENSIBLE ,nBlockAlign必须等于NumChannels * BitsPerSample / 8。
附加额外格式信息大小(Byte)以字节为单位,附加额外的格式信息WAVEFORMATEX结构。如果没有额外的信息需要由wFormatTag,这个参数必须被设置为零。最后,声音文件通常存储为.wave格式。
2、录音相关API函数,结构,消息
对于录音设备来说,windows 提供了一组wave***的函数,比较重要的有以下几个:
打开录音设备函数
MMRESULT waveInOpen( LPHWAVEIN phwi, //输入设备句柄 UINT uDeviceID, //输入设备ID LPWAVEFORMATEX pwfx, //录音格式指针 DWORD dwCallback, //处理MM_WIM_***消息的回调函数或窗口句柄,线程ID DWORD dwCallbackInstance, DWORD fdwOpen //处理消息方式的符号位 );
为录音设备准备缓存函数 :
MMRESULT waveInPrepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT bwh );
给输入设备增加一个缓存 :
MMRESULT waveInAddBuffer( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh );
开始录音函数 :
MMRESULT waveInStart( HWAVEIN hwi );
清除缓存函数 :
MMRESULT waveInUnprepareHeader( HWAVEIN hwi,LPWAVEHDR pwh, UINT cbwh);
停止录音函数 :
MMRESULT waveInReset( HWAVEIN hwi );
关闭录音设备函数:
MMRESULT waveInClose( HWAVEIN hwi ); Wave_audio数据格式: typedef struct { WORD wFormatTag; //数据格式,一般为WAVE_FORMAT_PCM即脉冲编码 WORD nChannels; //声道 DWORD nSamplesPerSec; //采样频率 DWORD nAvgBytesPerSec; //每秒数据量 WORD nBlockAlign; //块大小 WORD wBitsPerSample;//样本大小 WORD cbSize; } WAVEFORMATEX; waveform-audio 缓存格式: typedef struct { LPSTR lpData; //内存指针 DWORD dwBufferLength;//长度 DWORD dwBytesRecorded; //已录音的字节长度 DWORD dwUser; DWORD dwFlags; DWORD dwLoops; //循环次数 struct wavehdr_tag * lpNext; DWORD reserved; } WAVEHDR;
相关消息
MM_WIM_OPEN:打开设备时消息,在此期间我们可以进行一些初始化工作
MM_WIM_DATA:当缓存已满或者停止录音时的消息,处理这个消息可以对缓存进行重新分配,实现不限长度录音
MM_WIM_CLOSE:关闭录音设备时的消息。
相对于录音来说,回放就简单的多了,用到的函数主要有以下几个:
打开回放设备函数
MMRESULT waveOutOpen( LPHWAVEOUT phwo, UINT uDeviceID, LPWAVEFORMATEX pwfx, DWORD dwCallback, DWORD dwCallbackInstance, DWORD fdwOpen );
为回放设备准备内存块函数
MMRESULT waveOutPrepareHeader( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
写数据(放音)函数
MMRESULT waveOutWrite( HWAVEOUT hwo, LPWAVEHDR pwh, UINT cbwh );
相应的也有三个消息,用法跟录音的类似。
3.控制台应用程序Win32
根据上面的信息,简要地写了一个Win32控制台应用程序,可以实现录音和回放。代码如下:
#include "stdafx.h" #include "stdlib.h" #include <windows.h> #include <stdio.h> #include <mmsystem.h> #pragma comment(lib, "winmm.lib") #define BUFFER_SIZE (44100*16*2/8*5) // 录制声音长度 #define FRAGMENT_SIZE 1024 // 缓存区大小 #define FRAGMENT_NUM 4 // 缓存区个数 static unsigned char buffer[BUFFER_SIZE] = {0}; static int buf_count = 0; // 函数定义 void CALLBACK waveInProc(HWAVEIN hwi,UINT uMsg,DWORD_PTR dwInstance,DWORD_PTR dwParam1,DWORD_PTR dwParam2); // 录音回调函数 void CALLBACK waveInProc(HWAVEIN hwi,UINT uMsg,DWORD_PTR dwInstance,DWORD_PTR dwParam1,DWORD_PTR dwParam2) { LPWAVEHDR pwh = (LPWAVEHDR)dwParam1; if ((WIM_DATA==uMsg) && (buf_count<BUFFER_SIZE)) { int temp = BUFFER_SIZE - buf_count; temp = (temp>pwh->dwBytesRecorded) ? pwh->dwBytesRecorded: temp; memcpy(buffer+buf_count, pwh->lpData, temp); buf_count += temp; waveInAddBuffer(hwi, pwh, sizeof(WAVEHDR)); } } void CALLBACK waveOutProc( HWAVEOUT hwo,UINT uMsg,DWORD_PTR dwInstance,DWORD_PTR dwParam1,DWORD_PTR dwParam2); // 放音回调函数 void CALLBACK waveOutProc( HWAVEOUT hwo,UINT uMsg,DWORD_PTR dwInstance,DWORD_PTR dwParam1,DWORD_PTR dwParam2) { if (WOM_DONE == uMsg) { buf_count = BUFFER_SIZE; } } // 入口 int _tmain(int argc, _TCHAR* argv[]) { /* 录音*/ // Device int nReturn = waveInGetNumDevs();//定义输入设备的数目 printf("输入设备数目:%d\n", nReturn); //识别输入的设备 for (int i=0; i<nReturn; i++) { WAVEINCAPS wic; //WAVEINCAPS结构描述波形音频输入设备的能力 waveInGetDevCaps(i, &wic, sizeof(WAVEINCAPS)); //waveInGetDevCaps功能检索一个给定的波形音频输入设备的能力 printf("#%d\t设备名:%s\n", i, wic.szPname); } // open HWAVEIN hWaveIn;//波形音频数据格式Wave_audio数据格式 WAVEFORMATEX wavform;//WAVEFORMATEX结构定义了波形音频数据格式。包括在这个结构中唯一的格式信息,共同所有波形音频数据格式。对于需要额外的信息的格式,这个结构包含在另一个结构的第一个成员,以及与其他信息 wavform.wFormatTag = WAVE_FORMAT_PCM; //WAVE_FORMAT_PCM即脉冲编码 wavform.nChannels = 2; // 声道 wavform.nSamplesPerSec = 44100; // 采样频率 wavform.nAvgBytesPerSec = 44100*16*2/8; // 每秒数据量 wavform.nBlockAlign = 4; wavform.wBitsPerSample = 16; // 样本大小 wavform.cbSize = 0; //大小,以字节,附加额外的格式信息WAVEFORMATEX结构 //打开录音设备函数 waveInOpen(&hWaveIn, WAVE_MAPPER, &wavform, (DWORD_PTR)waveInProc, 0, CALLBACK_FUNCTION); //识别打开的录音设备 WAVEINCAPS wic; waveInGetDevCaps((UINT_PTR)hWaveIn, &wic, sizeof(WAVEINCAPS)); printf("打开的输入设备:%s\n", wic.szPname); // prepare buffer static WAVEHDR wh[FRAGMENT_NUM]; for (int i=0; i<FRAGMENT_NUM; i++) { wh[i].lpData = new char[FRAGMENT_SIZE]; wh[i].dwBufferLength = FRAGMENT_SIZE; wh[i].dwBytesRecorded = 0; wh[i].dwUser = NULL; wh[i].dwFlags = 0; wh[i].dwLoops = 1; wh[i].lpNext = NULL; wh[i].reserved = 0; //为录音设备准备缓存函数: //MMRESULT waveInPrepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT bwh ); waveInPrepareHeader(hWaveIn, &wh[i], sizeof(WAVEHDR)); //给输入设备增加一个缓存: //MMRESULT waveInAddBuffer( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh ); waveInAddBuffer(hWaveIn, &wh[i], sizeof(WAVEHDR)); } // record printf("Start to Record...\n"); buf_count = 0; //刚开始录音的时候缓冲区的个数初始化为 //开始录音函数 //MMRESULT waveInStart( HWAVEIN hwi ); waveInStart(hWaveIn); //开始录音 while (buf_count < BUFFER_SIZE) { Sleep(1); } printf("Record Over!\n\n"); waveInStop(hWaveIn);//waveInStop功能停止的波形音频输入 //停止录音函数: //MMRESULT waveInReset( HWAVEIN hwi ); waveInReset(hWaveIn);//停止录音 //清除缓存函数: //MMRESULT waveInUnprepareHeader( HWAVEIN hwi,LPWAVEHDR pwh, UINT cbwh); for (int i=0; i<FRAGMENT_NUM; i++) { waveInUnprepareHeader(hWaveIn, &wh[i], sizeof(WAVEHDR)); delete wh[i].lpData; } //关闭录音设备函数: //MMRESULT waveInClose( HWAVEIN hwi ); waveInClose(hWaveIn); //system("pause"); printf("\n"); /* 放音*/ // Device nReturn = waveOutGetNumDevs(); //定义输出设备的数目 printf("\n输出设备数目:%d\n", nReturn); for (int i=0; i<nReturn; i++) { WAVEOUTCAPS woc; //WAVEINCAPS结构描述波形音频输出设备的能力 waveOutGetDevCaps(i, &woc, sizeof(WAVEOUTCAPS)); printf("#%d\t设备名:%s\n", i, wic.szPname); } // open HWAVEOUT hWaveOut;//打开回放设备函数 waveOutOpen(&hWaveOut, WAVE_MAPPER, &wavform, (DWORD_PTR)waveOutProc, 0, CALLBACK_FUNCTION); WAVEOUTCAPS woc; //WAVEINCAPS结构描述波形音频输出设备的能力 waveOutGetDevCaps((UINT_PTR)hWaveOut, &woc,sizeof(WAVEOUTCAPS)); printf("打开的输出设备:%s\n", wic.szPname); // prepare buffer WAVEHDR wavhdr; wavhdr.lpData = (LPSTR)buffer; wavhdr.dwBufferLength = BUFFER_SIZE; //MMRESULT waveOutPrepareHeader(HWAVEOUT hwo,LPWAVEHDR pwh,UINT cbwh); wavhdr.dwFlags = 0; //为回放设备准备内存块函数 wavhdr.dwLoops = 0; waveOutPrepareHeader(hWaveOut, &wavhdr, sizeof(WAVEHDR)); // play printf("Start to Play...\n"); buf_count = 0; //MMRESULT waveOutWrite(HWAVEOUT hwo,LPWAVEHDR pwh,UINT cbwh); waveOutWrite(hWaveOut, &wavhdr, sizeof(WAVEHDR)); //写数据(放音)函数 //声音文件还没有放完的话运行不能退出 while (buf_count < BUFFER_SIZE) { Sleep(1); } // clean waveOutReset(hWaveOut); //停止放音 //MMRESULT waveOutPrepareHeader(HWAVEOUT hwo,LPWAVEHDR pwh,UINT cbwh); waveOutUnprepareHeader(hWaveOut, &wavhdr, sizeof(WAVEHDR)); //为回放设备准备内存块函数 waveOutClose(hWaveOut); //关闭放音设备函数 printf("Play Over!\n\n"); printf("Play Over!\n\n"); return 0; }
最后在说明一下,这种方式录音存储的声音数据和播放的数据都是在内存中,没有文件操作,也就是说用这种方式是不能实现录音文件的存储。这种方式将在下一篇中通过MFC来实现。