自己动手做简单的播放器和录音机

    最近因为要做一篇关于数字音频方面的小论文,才在网上到处看了看,发现有教做播放器和录音机的,觉得很有兴趣,于是就干了起来。做播放器,我以前搞过,在VC里拖个控件就搞定了,觉得没有什么神秘的,但是录音机还真没有试过,于是找了一下,用API做还是比较有成就感的,看了一位网友的例程,我觉得写的很好,而且在我的程序里也借用了一些好的代码。在这里向他表示感谢,也避免了招袭之嫌,只是我觉得他写的有些不太清楚,如果是一次没有接触过多媒体编程的人看了,也不太容易明白。而且该说的地方也没有交待清楚。只是列出了几个API函数,另外,一些不必要的功能增加了代码的复杂性。所以,我打算把这种编程的原理用自己的语言再说一下,一来加深印象,再来希望可以做为一个借鉴吧。
    好了,我们现在开始,关于数字音频技术,我不想多讲了,大家都可以查得到,现在只是看看我们的程序。主要完成录音的功能,录音结束后,可以播放。做出来的整体效果,我希望是这个样子的:


一:原理篇:
    用WIN API对音频设备进行编程大至可以分为以下几个步骤:
    录音:
        1:建立存放声音的数据缓冲区。由于在经过麦克风之后的声音已经变成了数字的信号,以字节的方式呈现,所以,我们要为之建立的缓冲区不需要什么特别的类型只要是字节型的就好,在VC里可以用一个PBYTE类型的指针指向这个缓冲区,以备后用。这里要提一下大小的问题。看到我的程序说明了吗?只有10秒钟?为什么?,呵呵,我只是想简单而以,告诉大家原理就行了,如果想无时限的录下去怎么办?且听下文分解(呵呵)。
    下面把这一步的程序列出来:
pRecordBuffer = (PBYTE)malloc(RECORD_BUFFER_LEN);
    其中RECORD_BUFFER_LEN就是定义的缓冲区大小,我的是40960Bytes

        2:建立(选择)音频输入设备。这一步是很显然的,如果想录音的话,肯定要有音源,这样我们就必需做一个选择。好的,我们先来看看MSDN上对这个函数的定义:
The waveInOpen function opens the given waveform-audio input device for recording.

MMRESULT waveInOpen(
  LPHWAVEIN       phwi,      
  UINT_PTR       uDeviceID,  
  LPWAVEFORMATEX pwfx,       
  DWORD_PTR      dwCallback, 
  DWORD_PTR      dwCallbackInstance, 
  DWORD          fdwOpen     
);
第一个参数是WAVEIN句柄,也就是以后唯一表示这个设备的句柄。它将做为参数传给相应的处理函数,这一点我们自然会想到。
第二个参数是设备ID,因为有可能会有很多的输入设备,所以,要在这里做一个选择。
uDeviceID 
Identifier of the waveform
-audio input device to open. It can be either a device identifier or a handle of an open waveform-audio input device. You can use the following flag instead of a device identifier. 

VALUE:    WAVE_MAPPER:
MEANING:The function selects a waveform
-audio input device capable of recording in the specified format.
上面的意思是,你可以选择一个ID号,或者一个已经打开的设备句柄,也可以是一个叫做WAVE_MAPPER的值,它表示会自动选择一个设备。呵呵,看到这你就会笑了,那就让它自己整吧!
第三个参数是一个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; 


其中各个参数的意思我不想一一说了,下面给你程序看看,聪明的你一定能够明白它是什么意思了。好,现在看这一步的代码:
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表示位。
看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. 
通过上面的分析,可以知道,它可以指向函数,窗口,线程。用来干什么的呢?
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; 
乖乖,又是这些东西,看不下去,没关系,让你看看我们真实的代码,就明白了:
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就好!
函数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.
还要用上面的这个函数发送到输入设备。真是麻烦。
waveInAddBuffer(hWaveIn, pWaveHdr, sizeof (WAVEHDR));
现在可以了吧,还不行?
(靠!)
呵呵,开个玩笑,现在可以了!
waveInStart(hWaveIn);
终于到这一步了。
好了,现在我们的程序已经在录音了。那么我们怎么知道,已经开始录了呢?怎么知道已经录好了呢?录好了什么时候关呢?
呵呵,现在该上面的中兄弟出场了。
开始:
void CSoundRecordDlg::OnMM_WIM_OPEN(UINT wParam, LONG lParam) 
{
    
// 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
结束:
void CSoundRecordDlg::OnMM_WIM_CLOSE(UINT wParam, LONG lParam) 
{
      TRACE(
"End Recording");    
}
呵呵,好了有了这三个消息,我们就好办了,现在我们来完成它们与消息的映射:在DLG的头文件中加入对三大处理函数的声明:
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的实现文件中完成它们与消息之间的映射:
ON_MESSAGE(MM_WIM_OPEN,OnMM_WIM_OPEN)
ON_MESSAGE(MM_WIM_DATA,OnMM_WIM_DATA)
ON_MESSAGE(MM_WIM_CLOSE,OnMM_WIM_CLOSE)
好了,大功告成,呵呵,剩下的工作就是为按钮加事件处理函数了:
void CSoundRecordDlg::OnButtonStartRecord() 
{
    
// TODO: Add your control notification handler code here
    
    ((CWnd 
*)(this->GetDlgItem(IDC_BUTTON_START_RECORD)))->EnableWindow(FALSE);
    bRecording 
= TRUE;

    pRecordBuffer 
= (PBYTE)malloc(RECORD_BUFFER_LEN);
    
    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);

    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));
    
    waveInAddBuffer(hWaveIn, pWaveHdr, 
sizeof (WAVEHDR));

    dwDataLength 
= 0;

    waveInStart(hWaveIn);    
//start recording
}

void CSoundRecordDlg::OnButtonEndRecord() 
{
    
// TODO: Add your control notification handler code here
    waveInReset(hWaveIn);
    ((CWnd 
*)(this->GetDlgItem(IDC_BUTTON_START_RECORD)))->EnableWindow(TRUE);
    
char strDatalen[20];
    wsprintf(strDatalen,
"%d Bytes",dwDataLength);    
    m_DataLength.SetWindowText(strDatalen);
    TRACE(
"record ended!");
}

        好了,到这里,基本上已经表达清楚了,至于播放,大家可以参照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芜湖
posted on 2006-03-12 13:27  Stone_石头  阅读(3108)  评论(4编辑  收藏  举报