简介
这是我很早以前的大学毕业设计,忽然间找到贴出来以纪念自己的纯真年代...但是因为CSDN不给面子所以导致短短的一篇文章贴了足足7次..他老提时说文章超过了64K,老大,拜托,那是算上了里面的图片大小吧...:-(
本文简单介绍了声卡的工作原理
,
录音的原理以及数字音频的基本知识并且利用
Windows
提供的
Waveform Aduio APIs
以及
Multimedia File I/O APIs
实现一个
Windows
环境下的麦克风录音以及将录音文件保存成
.wav
文件的简单系统
.
关键字
Waveform Aduio APIs, Multimedia File I/O APIs,waveInXXX,
mmioXXX,
麦克风
,
录音
,
波形文件
,VC6++
要深入的了解麦克风录音的实现我们必须了解声卡的工作原理
,麦克风录音的原理以及了解相关的编程接口,下面我们将慢慢道来…
1.
声卡的工作原理
声卡的工作原理其实很简单
,我们知道,麦克风和喇叭所用的都是模拟信号,而电脑所能处理的都是数字信号,两者不能混用,声卡的作用就是实现两者的转换。从结构上分,声卡可分为模数转换电路和数模转换电路两部分,模数转换电路负责将麦克风等声音输入设备采到的模拟声音信号转换为电脑能处理的数字信号,而数模转换电路负责将电脑使用的数字声音信号转换为喇叭等设备能使用的模拟信号,就这么简单
。
上图就是一块典型的声卡
,
Mic
插口
用于连接麦克风
,
通过它可以录制外界的声音
2.
数字音频基础知识
麦克风录音的过程其实就是将模拟信号转化成数字信号的过程
,
其中涉及的一些概念如下
:
1.
采样率
(Sampling Rate)
采样率指声卡在一秒之中对声音
(
波形
)
作记录的次数
,
根据研究声音播出时的质量常常只能达到采样率的一半
,
因此必须采取双倍的采样率才能将声音标准重现
.
也就是只要采样率大于原始信号频率的两倍以上即可减低错误
,
达到和原始声音差不多的质量
.
人的听力大概是
20KHZ,
所以高品质的采样率应为其两倍以上
.
当声音来源为音乐时
,
因为它所横跨的频率变化极为宽广
,
通常以
44.1KHZ
的频率为
CD
音乐采样率的标准
.
但是若以语言为主由于人说话的语音大概是
10KHZ,
因此加倍采样
,
只取
22KHZ
即可
,
采样率越高所记录下来的音质就越清晰
,
当然
,
越高的采样所记录下的文件就越大
.
2.
采样位
解析度决定了采样的音波是否能保持原来的形状
,
越接近原型则需解析度越高
,
若以
8
位来采样的话其能表达的组合种类是
2
的
8
次方
,
即
256,
表示用
8
位的采样大小能分辨出
256
个层次的声音
,
若用
16
位来采样
,
则能分辨的差异将高达
2
的
16
次方
,
为
65536,
其精度自然大为提高
.16
位
,8
位采样的差别在于动态范围的宽窄
,
动态范围宽广
,
音量起伏的大小变化就能够更精细的被记录下来
,
如此一来不论是细微的声音或是强烈的动感震撼
,
都可以表现的淋漓尽致
,
而
CD
音质的采样规格正式
16
位采样的规格
.
3.
量化误差
(Quantization error)
在采样的过程中
,
不断连续变化的模拟信号要用数字化的数值来表示
,
这样的过程就会发生所谓的量化误差
(Quantization error).
所谓的量化误差指的是实际的信号的振幅
(smplitude)
和数字化之后所的数字之间的差异
.
如果用将数字信号还原成模拟信号的角度看
,
量化误差就是失真
(Distortion).
我们可以用增加采样大小的方式来降低量化误差
,
也就是更多的位
(bits)
来表示一个采样信号
,
这样可以提高精度
.
4.
量化
(Quantization),
线性量化法
(Linear quantization)
和非线性量化法
(Nonlinear quantization)
所谓的量化
(Quantization)
就是将模拟信号所代表的连续范围分成一段一段的区间
(Interval),
每一段区间我们定义一个数字化的值
.
区间的数目是跟采样大小有关
,
举例来说
,
有一种最简单的量化法称为
”
线性量化法
”(Linear quantization),
这种量化法采用等距离的间隔空间
,
架设一个讯号它的最大值是
5.0,
采样大小为
3
位
,
则每个量化区间就时
5.0/2^3,
也就是
0.625
单位
.
另外一种相反的量化方法就是
”
非线性量化法
”(Nonlinear quantization),
这种量化法采用不同的间隔空间
.
以
”
对数量化法
”(Logarithm quantization)
为例
.
低振幅范围的量化区间就比高振幅的范围的区间较为接近
,
用这种量化的法产生的结果就是在低振幅时我们会得到佳好的效果
.
通常如果使用同样的采样大小
,
非线性量化法会比线性量化法得到更好的声音品质
.
但是如果是要对声音做滤波
(filtered)
或一些运算的时候
,
使用线性量化法会比较容易处理
.
5.
声音强度
波形振幅的平方.两个声音强度上的差常以分贝
(db)
为单位来度量
,计算公式如下:
20*log(A1/A2)
分贝
,
A1,A2
为两个声音的振幅
.
a.
如果采样大小为
8
位
,则采样的动态范围为
20*log(256)
分贝
=48db;
b.
如果样本大小为
16
位
,则采样动态范围为
20*log(65536)
大约是
96
分贝
,接近了人听觉极限和痛苦极限,是再线音乐的理想范围,
windows
同时支持
8
位和
16
位的采样大小
.
6.
音频编码方法
目前已经发展了许多音频编码的方法用以减少存储量或是传输的时间
,
以下所列为两种较普遍的编码方法
:
a.PCM(Pulse code modulation);
脉冲编码调制
,即对波形按照固定周期频率采样。为了保证采样后数据质量,采样频率必须是样本声音最高频率的两倍,这就是
Nyquist
频率
.
b.ADPCM(Adaptive delta pulse modulation).
3.
RIFF
文件结构和
WAVE
文件格式
Windows
支持两种
RIFF(Resource Interchange File Format,"
资源交互文件格式
")
格式的音频文件:
MIDI
的
RMID
文件和波形音频文件格式
WAVE
文件,其中在计算机领域最常用的数字化声音文件格式是后者,它是微软专门为
Windows
系统定义的波形文件格式(
Waveform Audio
),由于其扩展名为
"*.wav"
,因而该类文件也被称为
WAVE
文件。
为了突出重点,有的放矢,本文涉及到的声音文件所指的就是
WAVE
文件。常见的
WAVE
语音文件主要有两种,分别对应于单声道(
11.025KHz
采样率、
8Bit
的采样值)和双声道(
44.1KHz
采样率、
16Bit
的采样值)。这里的采样率是指声音信号在进行
"
模→数
"
转换过程中单位时间内采样的次数。采样值是指每一次采样周期内声音模拟信号的积分值。对于单声道声音文件,采样数据为八位的短整数(
short int 00H-FFH
);而对于双声道立体声声音文件,每次采样数据为一个
16
位的整数(
int
),高八位和低八位分别代表左右两个声道。
WAVE
文件数据块包含以脉冲编码调制(
PCM
)格式表示的样本。在进行声音编程处理以前,首先让我们来了解一下
RIFF
文件和
WAVE
文件格式。
RIFF
文件结构可以看作是树状结构,其基本构成是称为
"
块
"
(
Chunk
)的单元,每个块有
"
标志符
"
、
"
数据大小
"
及
"
数据
"
所组成,块的结构如图
2
所示:
块的标志符(
4BYTES
)
|
数据大小
(
4BYTES
)
|
数据
|
图
2
从上图可以看出,其中
"
标志符
"
为
4
个字符所组成的代码,如
"RIFF"
,
"LIST"
等,指定块的标志
ID
;数据大小用来指定块的数据域大小,它的尺寸也为
4
个字符;数据用来描述具体的声音信号,它可以由若干个子块构成,一般情况下块与块是平行的,不能相互嵌套,但是有两种类型的块可以嵌套子块,他们是
"RIFF"
或
"LIST"
标志的块,其中
RIFF
块的级别最高,它可以包括
LIST
块。另外,
RIFF
块和
LIST
块与其他块不同,
RIFF
块的数据总是以一个指定文件中数据存储格式的四个字符码(称为格式类型)开始,如
WAVE
文件有一个
"WAVE"
的格式类型。
LIST
块的数据总是以一个指定列表内容的
4
个字符码(称为列表类型)开始,例如扩展名为
".AVI"
的视频文件就有一个
"strl"
的列表类型。
RIFF
和
LIST
的块结构如下:
RIFF/LIST
标志符
|
数据
1
大小
|
数据
1
|
格式
/
列表类型
|
数据
|
图
3
WAVE
文件是非常简单的一种
RIFF
文件,它的格式类型为
"WAVE"
。
RIFF
块包含两个子块,这两个子块的
ID
分别是
"fmt"
和
"data",
其中
"fmt"
子块由结构
PCMWAVEFORMAT
所组成,其子块的大小就是
sizeofof(PCMWAVEFORMAT),
数据组成就是
PCMWAVEFORMAT
结构中的数据。
WAVE
文件的结构如下图
4
所示:
标志符(
RIFF
)
|
数据大小
|
格式类型(
"WAVE"
)
|
"fmt"
|
Sizeof(PCMWAVEFORMAT)
|
PCMWAVEFORMAT
|
"data"
|
声音数据大小
|
声音数据
|
图
4
PCMWAVEFORMAT
结构定义如下:
typedef struct { WAVEFORMAT wf; //
波形格式
,
前面已经提过了
;
WORD wBitsPerSample;// WAVE
文件的采样大小;
}PCMWAVEFORMAT "data"子块包含WAVE文件的数字化波形声音数据,其存放格式依赖于"fmt"子块中wFormatTag成员指定的格式种类,在多声道WAVE文件中,样本是交替出现的。如16bit的单声道WAVE文件和双声道WAVE文件的数据采样格式分别如图5所示:
16位单声道:
采样一
|
采样二
|
……
|
低字节
|
高字节
|
低字节
|
高字节
|
……
|
|
|
|
|
|
16
位双声道:
采样一
……
|
左声道
|
右声道
|
……
|
低字节
|
高字节
|
低字节
|
高字节
|
……
|
|
|
|
|
|
图
5
4.
硬件抽象层
(
HAL,
Hardware Abstraction
Layer
)
HAL
是一个可加载的核心模块
(HAL.dll)
,它为运行在
Windows NT
架构
(
包括
WindowsNT4.0,Windows2000,WindowsXP)
上
的硬件平台提供低级接口
,
HAL
隐藏各种与硬件有关的细节
,例如:
I/O
接口
,中断控制器,声卡…这样的话如果用户需要访问声卡硬件的话只能通过该声卡的驱动程序来实现,声卡驱动程序再调用
HAL
中的相应例程来实现
,下图显示了
HAL
,声卡驱动程序,
Waveform Audio APIs
,我们的麦克录音程序之间的关系:
|
5.
Waveform Audio
Waveform Audio APIs
是
Microsoft
提供给广大
Win32
程序员用来给自己的应用程序添加声音支持的一套强大的
API,
它提供的功能如下
:
1.
打开
/
关闭
/
查询声音设备
;
2.
播放波形文件
;
3.
设置播放速度
;
4.
播放进度控制
;
5.
录音
;
6.
得到当前的播放位置
;
7.
调节音量
.
下面简单介绍一下这套
API
提供的主要函数
:
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 );
typedef struct {
WORD wFormatTag; //
数据格式,一般为
WAVE_FORMAT_PCM
即
//
脉冲编码
WORD nChannels; //
声道
DWORD nSamplesPerSec; //
采样频率
DWORD nAvgBytesPerSec; //
每秒数据量
WORD nBlockAlign;
WORD wBitsPerSample; //
样本大小
WORD cbSize;
} WAVEFORMATEX;
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
:关闭录音设备时的消息。
5. Multimedia File I/O
Multimedia File I/O APIs
是
Microsoft
提供的另外一套强大的针对媒体文件
I/O
的
API,
我们知道许多像
MediaPlay,RealOne
这样的多媒体程序对媒体文件的读写性能要求很高
,
它们几乎要求实时的将磁盘上的媒体文件以流的形式读入
,
但是对于一般的文件
I/O
形式如图
1:
1.文件从磁盘上被读入操作系统的File I/O的buffer;
2.
然后拷贝到应用程序自己的
buffer
中
;
3.
应用程序这时候才能读取文件内容
.
上述的过程对于多媒体应用程序来说是低效的而且浪费宝贵的内存资源,如果文件和大的话势必还要采取分段读取等机制,
Multimedia File I/O
采取了一种直接存取机制
(
如图
2),
使得应用程序可以直接读取操作系统的
File I/O buffer,
大大提高了效率
.
后面我们会利用此套
API
实现录音文件的存储
.
6.
麦克录音系统简介
本文实现的麦克录音系统将具备以下功能
:
1.
录制用户通过麦克风发出的声音
;
这将利用到
Waveform APIs,
流程如下
:
a.
打开录音设备
waveInOpen;
b.
准备
wave
数据头
waveInPrepareHeader;
c.
准备数据块
waveInAddBuffer;
d.
开始录音
waveInStart;
e.
停止录音
(waveInReset);
f.
关闭录音设备
(waveInClose);
g.
当开始录音后当
buffer
已满时
,
将收到
MM_WIM_DATA
消息
,
处理该
消息可以保存已录好数据
.
2.
根据用户的声音强弱动态显示声音波形
;
这主要通过
GDI
函数来实现
.
3.
将用户通过麦克风发出的声音录制成
wav
文件保存
.
这将利用到
Multimedia file I/O APIs.
a
.调用
mminoOpen
函数来打开
WAVE
文件
,
获取
HMMIO
类型的文件句柄
;
b
.根据
WAVE
文件的结构
,
调用
mmioRead
、
mmioWrite
和
mmioSeek
函数实现文件的读、写和定位操作
;
c
.调用
mmioClose
函数来关闭
WAVE
文件
.
7.
麦克录音系统的实现
(MicDemo)
下面是该系统的界面
:
对于录音来说最重要的就是CSoundIn类,下面就是该类的定义:
namespace
perdubug
{
//
prevent the name-space pollution
class
CSoundIn
{
public
:
BOOL __initMic();
//
get the best wave format supported by your sound card
//
and then i will use the format to capture sound.
void
__closeMic();
BOOL __openMic();
//
open device and begin to capture with the best format(when
//
invoke __initMic function then you will get the best format
//
supported by host's sound card
//
//
if your want to capture sound and export into a wav file please invoke this function
//
to tell me the full path then i will create the wav file.
//
void
__createOutputWaveFile(
const
TCHAR
*
lpszFileName);
//
if you invoke any member function return error/false please
//
use this function to get the result
DWORD __getLastError();
//
//
when the capture buffer is filled please invoke this function to 'add buffer'(Actually
//
you should create two-circular buffers,when 1st buffer is filled then switch to 2st,1st
//
buffer will be wrote into wav file.
//
void
AddBuffer();
virtual
~
CSoundIn();
friend CSoundIn
&
theSoundCapture();
private
:
BOOL GetBestWaveFormat(WAVEFORMATEX
&
waveFormatEx);
//
because sound card is one and only so i must limit the number of CSoundIn object,
//
but how to limit the class object nums?maybe put constructor into private scope is
//
a good idea,:-)
CSoundIn();
private
:
WAVEINCAPS m_WaveInDevCaps;
HWAVEIN m_WaveIn;
WAVEHDR m_WaveHeader;
WAVEFORMATEX m_WaveFormat;
UINT m_WaveInSampleRate;
int
m_NbMaxSamples;
UINT m_SizeRecord;
DWORD m_dwLastError;
enum
{ MAX_SIZE_INPUT_BUFFER
=
1
*
2
*
1024
}
;
//
samples * voie * size_samples
public
:
SHORT InputBuffer[MAX_SIZE_INPUT_BUFFER];
//
used for int FFT,many GUI application
//
want to display sound peak so..
BOOL m_bTerminateThread;
//
to 'kill' waveCallback function
BOOL m_bImportToWaveFile;
CWaveFile m_waveFile;
}
;
}
//
end namespace perdubug
对于将录音保存在WAV文件的工作主要是由CwaveFile类来完成.下面是该类的定义:
//
//
Encapsulates reading or writing sound data to or from a wave file
//
-----------------------------------------------------------------------------
class
CWaveFile
{
public
:
WAVEFORMATEX
*
m_pwfx;
//
Pointer to WAVEFORMATEX structure
HMMIO m_hmmio;
//
MM I/O handle for the WAVE
MMCKINFO m_ck;
//
Multimedia RIFF chunk
MMCKINFO m_ckRiff;
//
Use in opening a WAVE file
DWORD m_dwSize;
//
The size of the wave file
MMIOINFO m_mmioinfoOut;
DWORD m_dwFlags;
BOOL m_bIsReadingFromMemory;
BYTE
*
m_pbData;
BYTE
*
m_pbDataCur;
ULONG m_ulDataSize;
CHAR
*
m_pResourceBuffer;
protected
:
HRESULT ReadMMIO();
HRESULT WriteMMIO( WAVEFORMATEX
*
pwfxDest );
public
:
CWaveFile();
~
CWaveFile();
HRESULT Open( LPCTSTR strFileName, WAVEFORMATEX
*
pwfx, DWORD dwFlags );
HRESULT OpenFromMemory( BYTE
*
pbData, ULONG ulDataSize,
WAVEFORMATEX
*
pwfx, DWORD dwFlags );
HRESULT Close();
HRESULT Read( BYTE
*
pBuffer, DWORD dwSizeToRead, DWORD
*
pdwSizeRead );
HRESULT Write( UINT nSizeToWrite, BYTE
*
pbData, UINT
*
pnSizeWrote );
DWORD GetSize();
HRESULT ResetFile();
WAVEFORMATEX
*
GetFormat()
{
return
m_pwfx; }
;
}
;
我们有了这两个强有力的类的支持就可以开始我们的编程工作了….
1
.用VC6
++
建立一个MFC基于对话框的工程:MicDemo;
2
.添加我们的两个类CSoundIn,CwaveFile;
3
.当我们点击开始(Start)按钮的时候我们就要开始录音了…
void
CMicDemoDlg::OnStart()
{
m_btnStart.EnableWindow(FALSE);
if
(theSoundCapture().__initMic())
{
m_filePath.SetWindowText(_T(
"
yangchen.wav.
"
));
theSoundCapture().__createOutputWaveFile(_T(
"
yangchen.wav
"
));
if
(
!
theSoundCapture().__openMic())
{
::MessageBox(
this
->
m_hWnd,_T(
"
Can not open microphone!
"
), _T(
"
Error
"
),MB_OK
|
MB_ICONERROR);
return
;
}
}
m_btnStop.EnableWindow(TRUE);
//
设置定时器是为了画波形用的
SetTimer(
1
,
200
/**/
/*
start slow
*/
, NULL);
}
4
.在定时器的回调函数中画波形.
void
CMicDemoDlg::OnTimer(UINT nIDEvent)
{
if
(nIDEvent
==
1
)
{
static
const
int
xCon
=
13
;
static
const
int
yCon
=
13
;
static
const
int
wCon
=
623
;
static
const
int
hCon
=
80
;
CClientDC dc(
this
);
CBitmap Bitmap;
CBitmap
*
pbmOld
=
NULL;
CDC dcMem;
dcMem.CreateCompatibleDC(
&
dc);
Bitmap.CreateCompatibleBitmap(
&
dc,wCon,hCon);
pbmOld
=
dcMem.SelectObject(
&
Bitmap);
dcMem.PatBlt(
0
,
0
,wCon,hCon, WHITENESS);
dcMem.MoveTo(
0
,hCon
/
2
);
//
//
display incomming signal--key idea!
//
for
(
int
x
=
0
; x
<
wCon; x
++
)
//
display Input
{
dcMem.LineTo(x,(hCon
>>
1
)
-
(theSoundCapture().InputBuffer[x]
>>
7
));
}
dc.BitBlt(xCon,yCon,wCon,hCon,
&
dcMem,
0
,
0
, SRCCOPY);
dcMem.SelectObject(pbmOld);
dcMem.DeleteDC();
}
else
CDialog::OnTimer(nIDEvent);
}
5
.点击停止(Stop)按钮的时候停止录音和写WAV文件
void
CMicDemoDlg::OnStop()
{
m_btnStop.EnableWindow(FALSE);
theSoundCapture().__closeMic();
m_btnStart.EnableWindow(TRUE);
}
看完整段代码你可能会很奇怪怎么在CmicDemoDlg中居然都没有定义一个CSoundIn对象
??
呵呵,原因很简单,因为设备的独占性所以在一个时刻只能有一个CSoundIn对象存在(因为CSoundIn对象需要占据录音设备),所以我们必须限制程序员生成CSoundIn对象的数量,怎么限制呢
?
那就是把CSoundIn的构造函数放在private区域里面:
private
:
BOOL GetBestWaveFormat(WAVEFORMATEX
&
waveFormatEx);
//
because sound card is one and only so i must limit the number of CSoundIn object,
//
but how to limit the class object nums?maybe put constructor into private scope is
//
a good idea,:-)
CSoundIn();
这样的话就根本无法声明一个CSoundIn对象,不信你试一下在你的代码中写上:
CSoundIn soundInObj;
能编译通过吗
?
肯定是不能,那如何调用CSoundIn的成员函数呢
?
答案是通过一个全局函数:
//
global function,:-(
//
client can only through this function to use CSoundIn object
CSoundIn
&
theSoundCapture()
{
static
CSoundIn p;
return
p;
}
这时候你应该明白了为什么上面的代码中调用CSoundIn的成员函数的时候都是用theSoundCapture来做的原因了吧.