好了,我们现在开始,关于数字音频技术,我不想多讲了,大家都可以查得到,现在只是看看我们的程序。主要完成录音的功能,录音结束后,可以播放。做出来的整体效果,我希望是这个样子的:
一:原理篇:
用WIN API对音频设备进行编程大至可以分为以下几个步骤:
录音:
1:建立存放声音的数据缓冲区。由于在经过麦克风之后的声音已经变成了数字的信号,以字节的方式呈现,所以,我们要为之建立的缓冲区不需要什么特别的类型只要是字节型的就好,在VC里可以用一个PBYTE类型的指针指向这个缓冲区,以备后用。这里要提一下大小的问题。看到我的程序说明了吗?只有10秒钟?为什么?,呵呵,我只是想简单而以,告诉大家原理就行了,如果想无时限的录下去怎么办?且听下文分解(呵呵)。
下面把这一步的程序列出来:
2:建立(选择)音频输入设备。这一步是很显然的,如果想录音的话,肯定要有音源,这样我们就必需做一个选择。好的,我们先来看看MSDN上对这个函数的定义:
第二个参数是设备ID,因为有可能会有很多的输入设备,所以,要在这里做一个选择。
第三个参数是一个LPWAVEFORMATEX的指针,按照常理,这个指针肯定是指向一个WAVEFORMATEX的结构体,那么它是什么呢,从表面意思上,可能是“音频格式”的意思,不错,呵呵,它就是对声音格式的基本定义,让我们来看看它都定义了哪些东东:
The WAVEFORMATEX structure defines the format of waveform-audio data. Only format information common to all waveform-audio data formats is included in this structure. For formats that require additional information, this structure is included as the first member in another structure, along with the additional information.
typedef struct {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
} WAVEFORMATEX;
typedef struct {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
WORD wBitsPerSample;
WORD cbSize;
} WAVEFORMATEX;
其中各个参数的意思我不想一一说了,下面给你程序看看,聪明的你一定能够明白它是什么意思了。好,现在看这一步的代码:
waveform.wFormatTag=WAVE_FORMAT_PCM;
waveform.nChannels=1;
waveform.nSamplesPerSec=11025;
waveform.nAvgBytesPerSec=11025;
waveform.nBlockAlign=1;
waveform.wBitsPerSample=8;
waveform.cbSize=0;
waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,NULL,CALLBACK_WINDOW);
呵呵,清楚了吧,PCM一定不会陌生,11025一定是采样率。8表示位。waveform.nChannels=1;
waveform.nSamplesPerSec=11025;
waveform.nAvgBytesPerSec=11025;
waveform.nBlockAlign=1;
waveform.wBitsPerSample=8;
waveform.cbSize=0;
waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,NULL,CALLBACK_WINDOW);
看waveInOpen(&hWaveIn,WAVE_MAPPER,&waveform,(DWORD)this->m_hWnd,...)
上面红色标记的部分是什么意思,从函数定义上看,他是一个指针,
DWORD_PTR dwCallback:
Pointer to a fixed callback function, an event handle, a handle to a window, or the identifier of a thread to be called during waveform-audio recording to process messages related to the progress of recording. If no callback function is required, this value can be zero. For more information on the callback function, see waveInProc.
通过上面的分析,可以知道,它可以指向函数,窗口,线程。用来干什么的呢?Pointer to a fixed callback function, an event handle, a handle to a window, or the identifier of a thread to be called during waveform-audio recording to process messages related to the progress of recording. If no callback function is required, this value can be zero. For more information on the callback function, see waveInProc.
Process message related to the progress of recording.(在录音过程中产生的相关消息)
什么?在录音进程中还会有什么相关的消息呢?难首要我们自己发消息给它吗?显示不会的,WINDOWS是基于消息驱动的,如果你对消息还不太了解,我建议你去看《深入浅出MFC》一书,那里候老师帮我们把消息的内部运行机制全讲明白了,要不然的话,你会对这几个从天而降的消息产生敌意。呵呵。
我们处理这些消息的是什么呢?(DWORD)this->m_hWnd显然是一个窗口,代表我的对话框。所以,我们以后就可以在我的窗口类中定义相关的处理函数,然后再把这些处理函数和这些消息关联起来就OK了!
至于是些什么消息,我们下面会讲,这里只是把它们三个请出来和大家见个面,一会再让它们为大家表演:
老大:MM_WIM_OPEN
The MM_WIM_OPEN message is sent to a window when a waveform-audio input device is opened.
当一个录音开始的时候会产生这条消息。
老二:MM_WIM_DATA
The MM_WIM_DATA message is sent to a window when waveform-audio data is present in the input buffer and the buffer is being returned to the application. The message can be sent either when the buffer is full or after the waveInReset function is called.
当缓冲区满,或者waveInReset函数被调用的时候会产生这条消息。
老三:MM_WIM_CLOSE
The MM_WIM_CLOSE message is sent to a window when a waveform-audio input device is closed. The device handle is no longer valid after this message has been sent.
当录入设备被关闭的时候会产生这条消息。
好了,我们已经认识了消息,也得到了设备句柄,是不是现在就可以开始录音了呢?先别急,我们来看看MS是怎么告诉我们的,没有办法谁咱在WINDOWS下搞开发呢,所以,我们要学LINUX,我们自己做操作系统!(有点大了,呵呵)
After you open a waveform-audio input device, you can begin recording waveform-audio data. Waveform-audio data is recorded into application-supplied buffers specified by a WAVEHDR structure. These data blocks must be prepared before they are used;
在你打开输入设备后,在开始录音之前,你还要准备一个指向缓冲区的数据块(WAVHDR)。什么?不是有缓冲区了吗?没有办法,我们就暂且把这个结构称之为头或者引子吧,由它来指向我们开辟的那块缓冲区。是这样的吗?那就看看它长的是啥样吧:
The WAVEHDR structure defines the header used to identify a waveform-audio buffer.
typedef struct {
LPSTR lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD_PTR dwUser;
DWORD dwFlags;
DWORD dwLoops;
struct wavehdr_tag * lpNext;
DWORD_PTR reserved;
} WAVEHDR;
乖乖,又是这些东西,看不下去,没关系,让你看看我们真实的代码,就明白了:typedef struct {
LPSTR lpData;
DWORD dwBufferLength;
DWORD dwBytesRecorded;
DWORD_PTR dwUser;
DWORD dwFlags;
DWORD dwLoops;
struct wavehdr_tag * lpNext;
DWORD_PTR reserved;
} WAVEHDR;
pWaveHdr->lpData=(LPTSTR)pRecordBuffer;
pWaveHdr->dwBufferLength=RECORD_BUFFER_LEN;
pWaveHdr->dwBytesRecorded=0;
pWaveHdr->dwUser=0;
pWaveHdr->dwFlags=0;
pWaveHdr->dwLoops=1;
pWaveHdr->lpNext=NULL;
pWaveHdr->reserved=0;
waveInPrepareHeader(hWaveIn,pWaveHdr,sizeof(WAVEHDR));
呵呵,看到了吧,确实是指向我们的缓冲区的,剩下的参数,您对照着MSDN就好!pWaveHdr->dwBufferLength=RECORD_BUFFER_LEN;
pWaveHdr->dwBytesRecorded=0;
pWaveHdr->dwUser=0;
pWaveHdr->dwFlags=0;
pWaveHdr->dwLoops=1;
pWaveHdr->lpNext=NULL;
pWaveHdr->reserved=0;
waveInPrepareHeader(hWaveIn,pWaveHdr,sizeof(WAVEHDR));
函数waveInPrepareHeader是准备数据段。
那了,有这个东西,该可以录了吧,唉,不得不告诉你,在你发骠之前还有一步工作要做,那就是:
Use the waveInAddBuffer function to send buffers to the device driver. As the buffers are filled with recorded waveform-audio data, the application is notified with a window message, callback message, thread message, or event, depending on the flag specified when the device was opened.
还要用上面的这个函数发送到输入设备。真是麻烦。
(靠!)
呵呵,开个玩笑,现在可以了!
好了,现在我们的程序已经在录音了。那么我们怎么知道,已经开始录了呢?怎么知道已经录好了呢?录好了什么时候关呢?
呵呵,现在该上面的中兄弟出场了。
开始:
void CSoundRecordDlg::OnMM_WIM_OPEN(UINT wParam, LONG lParam)
{
// TODO: Add your message handler code here and/or call default
TRACE("Begin Recording!");
}
什么也没做啊?呵呵,你可以做一些其它的事情。{
// TODO: Add your message handler code here and/or call default
TRACE("Begin Recording!");
}
完成:
void CSoundRecordDlg::OnMM_WIM_DATA(UINT wParam, LONG lParam)
{
// TODO: Add your message handler code here and/or call default
AfxMessageBox("已经完成录音!");
dwDataLength += ((PWAVEHDR) lParam)->dwBytesRecorded ;
waveInClose (hWaveIn);
((CWnd *)(this->GetDlgItem(IDC_BUTTON_START_RECORD)))->EnableWindow(TRUE);
char strDatalen[20];
wsprintf(strDatalen,"%d Bytes",dwDataLength);
m_DataLength.SetWindowText(strDatalen);
TRACE("record closed!");
}
这里,我是得到一个表示录制文件大小的数据dwDataLength{
// TODO: Add your message handler code here and/or call default
AfxMessageBox("已经完成录音!");
dwDataLength += ((PWAVEHDR) lParam)->dwBytesRecorded ;
waveInClose (hWaveIn);
((CWnd *)(this->GetDlgItem(IDC_BUTTON_START_RECORD)))->EnableWindow(TRUE);
char strDatalen[20];
wsprintf(strDatalen,"%d Bytes",dwDataLength);
m_DataLength.SetWindowText(strDatalen);
TRACE("record closed!");
}
结束:
void CSoundRecordDlg::OnMM_WIM_CLOSE(UINT wParam, LONG lParam)
{
TRACE("End Recording");
}
呵呵,好了有了这三个消息,我们就好办了,现在我们来完成它们与消息的映射:在DLG的头文件中加入对三大处理函数的声明:{
TRACE("End Recording");
}
afx_msg void OnMM_WIM_OPEN(UINT wParam, LONG lParam);
afx_msg void OnMM_WIM_DATA(UINT wParam,LONG lParam);
afx_msg void OnMM_WIM_CLOSE(UINT wParam,LONG lParam);
在DLG的实现文件中完成它们与消息之间的映射:afx_msg void OnMM_WIM_DATA(UINT wParam,LONG lParam);
afx_msg void OnMM_WIM_CLOSE(UINT wParam,LONG lParam);
ON_MESSAGE(MM_WIM_OPEN,OnMM_WIM_OPEN)
ON_MESSAGE(MM_WIM_DATA,OnMM_WIM_DATA)
ON_MESSAGE(MM_WIM_CLOSE,OnMM_WIM_CLOSE)
好了,大功告成,呵呵,剩下的工作就是为按钮加事件处理函数了:ON_MESSAGE(MM_WIM_DATA,OnMM_WIM_DATA)
ON_MESSAGE(MM_WIM_CLOSE,OnMM_WIM_CLOSE)
好了,到这里,基本上已经表达清楚了,至于播放,大家可以参照MSDN和上面的输入反过程不难做出来,而且会加深对原理的理解呢。
今天讲的是API对音频进行处理的方法,当然这不是唯一的方法,而且有的时候不是一种很方便的方法,但是这可能是最底层的方法,但我们进行Socket网络上的语音通讯开发时,可能用这种方法会方便一点。
其它的方法包括:MCI,Direct Sound等,大家有兴趣不妨去看看,如果有什么心得也别忘了和俺分享。
本程序的原码和相关资料可以来E-MAIL索取。E-Mail:stonecrazyking@163.com
这个BLOG,可能会对您有所帮助。
http://blog.csdn.net/aoosang/archive/2005/08/17/456262.aspx
2006-03-12芜湖