上一篇中主要介绍了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来实现。