用VC自己动手做个录音机

搞了很久的程序,都是做业务系统之类的,还没有搞过多媒体方面的编程,今天动手做个录音机,了解一下声音相关的API,

window下面声音的编程主要有三种方式,

  • MCI,这种方式很简单,但是不够灵活
  • waveXXXX等低阶的声音API
  • 还有就是DirectSound

个人感觉使用waveXXX函数应该是最方便和最灵活的,也是比较简单的,

随便创建一个MFC基于对话框的工程,在窗口类里面增加几个成员

DWORD m_dwDataLength;        //数据长度
WAVEFORMATEX m_waveform;    //声音格式
HWAVEIN m_hWaveIn;            //音频输入句柄
HWAVEOUT m_hWaveOut;        //音频输出句柄
PBYTE m_pSaveBuffer;        //音频存储内存

WAVEHDR m_WAVEHDR1;
WAVEHDR m_WAVEHDR2;

char m_cbBuffer1[INP_BUFFER_SIZE];    //声音临时缓存1
char m_cbBuffer2[INP_BUFFER_SIZE];    //声音临时缓存2

 

 在构造函数对成员进行初始化

//录音机状态初始化
m_nRecordState = RECORD_STATE_INIT;
m_dwDataLength 
= 0;

//open waveform audo for input
//为声音输入设置格式
m_waveform.wFormatTag=WAVE_FORMAT_PCM;
m_waveform.nChannels
=1;
m_waveform.nSamplesPerSec
=11025;
m_waveform.nAvgBytesPerSec
=11025;
m_waveform.nBlockAlign
=1;
m_waveform.wBitsPerSample
=8;
m_waveform.cbSize
=0;    
    
m_pSaveBuffer 
= NULL;

 看看我的窗体

 

录音的处理函数

 


void CWaveRecordDlg::OnBnClickedBtnRecord()
{
    
//打开声音设备
    if(waveInOpen(&m_hWaveIn,WAVE_MAPPER,&m_waveform,
            (DWORD)m_hWnd,NULL,CALLBACK_WINDOW))
{
        MessageBeep(MB_ICONEXCLAMATION);
        MessageBox(_T(
"录制声音失败!"),
                    _T(
"错误"),MB_ICONEXCLAMATION|MB_OK);
        
return;
    }


    
//设置缓冲区
    m_WAVEHDR1.lpData = (LPTSTR)m_cbBuffer1;
    m_WAVEHDR1.dwBufferLength 
= INP_BUFFER_SIZE;
    m_WAVEHDR1.dwBytesRecorded 
= 0;
    m_WAVEHDR1.dwUser
=0;
    m_WAVEHDR1.dwFlags
=0;
    m_WAVEHDR1.dwLoops
=1;
    m_WAVEHDR1.lpNext
=NULL;
    m_WAVEHDR1.reserved
=0;

    m_WAVEHDR2.lpData
=(LPTSTR)m_cbBuffer2;
    m_WAVEHDR2.dwBufferLength 
= INP_BUFFER_SIZE;
    m_WAVEHDR2.dwBytesRecorded 
= 0;
    m_WAVEHDR2.dwUser
=0;
    m_WAVEHDR2.dwFlags
=0;
    m_WAVEHDR2.dwLoops
=1;
    m_WAVEHDR2.lpNext
=NULL;
    m_WAVEHDR2.reserved
=0;

    
//设置双缓冲
    waveInPrepareHeader(m_hWaveIn,&m_WAVEHDR1,sizeof(WAVEHDR));
    waveInPrepareHeader(m_hWaveIn,
&m_WAVEHDR2,sizeof(WAVEHDR));

    waveInAddBuffer(m_hWaveIn,
&m_WAVEHDR1,sizeof(WAVEHDR));
    waveInAddBuffer(m_hWaveIn,
&m_WAVEHDR2,sizeof(WAVEHDR));

    
//先随便分配个内存
    if(m_pSaveBuffer == NULL){
        free(m_pSaveBuffer);
        m_pSaveBuffer 
= NULL;
    }

    m_pSaveBuffer 
= (PBYTE)malloc(1);
    m_dwDataLength 
= 0;

    
//开始录音
    
// Begin sampling    
    waveInStart(m_hWaveIn);
}

 然后增加三个消息进行处理


LRESULT CWaveRecordDlg::OnMM_WIM_CLOSE(UINT wParam, LONG lParam)
{
    waveInUnprepareHeader(m_hWaveIn, 
&m_WAVEHDR1, sizeof (WAVEHDR)) ;
    waveInUnprepareHeader(m_hWaveIn, 
&m_WAVEHDR2, sizeof (WAVEHDR)) ;
    
    m_nRecordState 
= RECORD_STATE_STOP;
    SetButtonState();

    
return NULL;
}


LRESULT CWaveRecordDlg::OnMM_WIM_DATA(UINT wParam, LONG lParam)
{
    PWAVEHDR pWaveHdr 
= (PWAVEHDR)lParam;
    
    
if(pWaveHdr->dwBytesRecorded > 0){
        m_pSaveBuffer 
= (PBYTE)realloc(m_pSaveBuffer,m_dwDataLength + pWaveHdr->dwBytesRecorded);
        
if(m_pSaveBuffer == NULL){
            waveInClose (m_hWaveIn);
            MessageBeep (MB_ICONEXCLAMATION) ;
            AfxMessageBox(
"erro memory");
            
return NULL;
        }


        memcpy(m_pSaveBuffer
+m_dwDataLength , pWaveHdr->lpData,pWaveHdr->dwBytesRecorded);

        m_dwDataLength 
+= pWaveHdr->dwBytesRecorded;
    }


    
if(m_nRecordState == RECORD_STATE_STOPING){
        waveInClose(m_hWaveIn);        
    }


    waveInAddBuffer(m_hWaveIn, pWaveHdr, 
sizeof (WAVEHDR));

    
return NULL;
}


LRESULT CWaveRecordDlg::OnMM_WIM_OPEN(UINT wParam, LONG lParam)
{
    m_nRecordState 
= RECORD_STATE_RECING;
    SetButtonState();
    
return NULL;
}

 对录音处理的基本原理简单的谈一下,

先调用waveInOpen打开声音设备,设置WAVEHDR类型的缓冲区,其中lpData成员指向缓冲区的内存块,

可以分配动态内存也可以像我这里一样,分配在栈区的内存。接着就可以调用waveInPrepareHeader以及waveInAddBuffer,

调用 waveInStart开始录音,然后会发出消息MM_WIM_OPEN,如果一人缓冲区满了以后就会发出消息MM_WIM_DATA,

我们就可以在这个消息处理函数将声音内容拷到我们自己内存块保存起来,以及增加m_dwDataLength的长度,如果要停止录音,

只要调用一下waveInReset,然后会发出消息MM_WIM_DATA作最后处理,并且发出消息OnMM_WIM_CLOSE作点善后工作,

如在这里调用waveInUnprepareHeader去掉缓冲区,并且设置按钮的状态,lpData指向的是动态内存也可以在这里释放。

注:这里用了两个缓冲区,可以使声音连续,如果只使用一个缓冲区会导致声音有顿

现在看看声音播放,先看代码

void CWaveRecordDlg::OnBnClickedBtnPlay()
{
    
//打开声音播放句柄
    if(waveOutOpen(&m_hWaveOut,WAVE_MAPPER,&m_waveform,(DWORD)m_hWnd, NULL, CALLBACK_WINDOW)){
        MessageBeep(MB_ICONEXCLAMATION);
        MessageBox(_T(
"录制声音失败!"),_T("错误"),MB_ICONEXCLAMATION|MB_OK);
        
return;
    }
    
}


LRESULT CWaveRecordDlg::OnMM_WON_OPEN(UINT wParam,LONG lParam)
{
    memset(
&m_WAVEHDR1,0,sizeof(WAVEHDR));
    m_WAVEHDR1.lpData 
= (char *)m_pSaveBuffer;
//指向要播放的内存
    m_WAVEHDR1.dwBufferLength 
= m_dwDataLength;
//播放的长度
    m_WAVEHDR1.dwFlags 
= WHDR_BEGINLOOP | WHDR_ENDLOOP;
    m_WAVEHDR1.dwLoops 
= 1;

    waveOutPrepareHeader(m_hWaveOut,
&m_WAVEHDR1,sizeof(WAVEHDR));

    waveOutWrite(m_hWaveOut,
&m_WAVEHDR1,sizeof(WAVEHDR));

    m_nRecordState 
= RECORD_STATE_PLAYING;
    SetButtonState();
    
return NULL;
}


LRESULT CWaveRecordDlg::OnMM_WOM_DONE(UINT wParam,LONG lParam)
{
    PWAVEHDR pWaveHdr 
= (PWAVEHDR)lParam;
    waveOutUnprepareHeader (m_hWaveOut, pWaveHdr, 
sizeof (WAVEHDR)) ;
    waveOutClose (m_hWaveOut);
    
return NULL;
}


LRESULT CWaveRecordDlg::OnMM_WOM_CLOSE(UINT wParam,LONG lParam)
{
    m_nRecordState 
= RECORD_STATE_STOP;
    SetButtonState();
    
return NULL;
}

 声音的播放相对简单一点,在这里这种情况不需要用到双缓存技术,只要调用 waveOutOpen打开声音播放句柄,

然后响应 MM_WON_OPEN消息,设置缓冲区,就可以了

当然,如果是网络聊天的时候发出来的声音包应该不是这样一大块的内存,而是一小块一小块就得用到双缓存技术,

需要处理MM_WOM_DONE,以便重新加入声音包,可以顺畅的播放;在这里只是简单的去掉缓冲区

 然后现在就剩下保存成wav,以后读取wav文件了

首先看一下wav文件的格式,以下内容部分摘自window程序设计

 

表22-1 .WAV档案格式
偏移量 位元组 资料
0000 4 「RIFF」
0004 4 波形块的大小(档案大小减8)
0008 4 「WAVE」
000C 4 「fmt 」
0010 4 格式块的大小(16位元组)
0014 2 wf.wFormatTag = WAVE_FORMAT_PCM = 1
0016 2 wf.nChannels
0018 4 wf.nSamplesPerSec
001C 4 wf.nAvgBytesPerSec
0020 2 wf.nBlockAlign
0022 2 wf.wBitsPerSample
0024 4 「data」
0028 4 波形资料的大小
002C   波形资料

 

这是一种扩充自RIFF(Resource Interchange File Format:资源交换档案格式)的格式。RIFF是用於多媒体资料档案的万用格式,它是一种标记档案格式。在这种格式下,档案由资料「块」组成,而这些资料块则由前面4个字元的ASCII名称和4位元组(32位元)的资料块大小来确认。资料块大小值不包括名称和大小所需要的8位元组。

波形声音档案以文字字串「RIFF」开始,用来标识这是一个RIFF档案。字串後面是一个32位元的资料块大小,表示档案其余部分的大小,或者是小於8位元组的档案大小。

资料块以文字字串「WAVE」开始,用来标识这是一个波形声音块,後面是文字字串「fmt」-注意用空白使之成为4字元的字串-用来标识包含波形声音资料格式的子资料块。「fmt」字串的後面是格式资讯大小,这里是16位元组。格式资讯是WAVEFORMATEX结构的前16个位元组,或者,像最初定义时一样,是包含WAVEFORMAT结构的PCMWAVEFORMAT结构。

 格式资讯的後面是文字字串「data」,然後是32位元的资料大小,最後是波形资料本身。这些资料是按相同格式进行简单连结的样本,这与低阶波形声音设备上所使用的格式相同。如果样本大小是8位元,或者更少,那么每个样本有1位元组用於单声道,或者有2位元组用於立体声。如果样本大小在9到16位元之间,则每个样本就有2位元组用於单声道,或者4位元组用於立体声。对於立体声波形资料,每个样本都由左值及其後面的右值组成。

--------------------------------

基于上面的了解,就可以写一个读取wav包头的函数了

#define WAVE_HEADER_SIZE 44

int CWaveRecordDlg::read_wav_head(WAVEFORMATEX *wf,char **out_buffer,int *out_len,char *in_buffer,int in_len)
{
    
char *lp_pos;
    
int itmp;
    
    lp_pos 
= in_buffer;
    
if(in_buffer == NULL || in_len == 0 || in_len < WAVE_HEADER_SIZE || wf==NULL)
        
return 1;

    
if(strncmp(lp_pos,"RIFF",4)!=0)
        
return -1;
    lp_pos 
+= 4;

    itmp 
= *((int*)lp_pos);
    
if(itmp != (in_len-8))
        
return -1;
    lp_pos 
+= 4;

    
if(strncmp(lp_pos,"WAVEfmt ",8)!=0)
        
return -1;
    lp_pos 
+= 8;

    itmp 
= *((int*)lp_pos);
    
if(itmp != 16)
        
return -1;
    lp_pos 
+= 4;

    
/*格式信息*/
    memcpy(wf,lp_pos,
16);
    lp_pos 
+= 16;

    
if(strncmp(lp_pos,"data",4)!=0)
        
return -1;
    lp_pos 
+= 4;

    
//真正的数据长度
    *out_len = *((int*)lp_pos);
    lp_pos 
+= 4;

    
if(*out_len != (in_len - WAVE_HEADER_SIZE))
        
return 1;

    
*out_buffer = (char*)malloc(*out_len);
    
if(*out_buffer == NULL)
        
return -2;

    memcpy(
*out_buffer,lp_pos,*out_len);    
    
    
return 0;
}

 以及写wav头媒体信息

int CWaveRecordDlg::write_wav_head(WAVEFORMATEX *wf,char *in_buffer,int in_len,char **out_buffer, int *out_len)
{
    
char *buffer;
    
int *int_tmp,pos=0;

    
*out_len = WAVE_HEADER_SIZE + in_len;
    buffer 
= (char*)malloc(*out_len);
    
if(buffer == NULL) 
        
return -1;
    memcpy(buffer,
"RIFF",4);
    pos 
= 4;

    int_tmp 
= (int*)(buffer+pos);
    
*int_tmp = WAVE_HEADER_SIZE + in_len - 8;/*波形块的大小(档案大小减8)*/
    pos 
+= 4;

    memcpy(buffer
+pos,"WAVEfmt ",8);
    pos 
+= 8;

    int_tmp 
= (int*)(buffer+pos);
    
*int_tmp = 16;
    pos 
+= 4;

    
/*格式信息*/
    memcpy(buffer
+pos,wf,16);
    pos 
+= 16;

    memcpy(buffer
+pos,"data",4);
    pos 
+= 4;

    int_tmp 
= (int*)(buffer+pos);
    
*int_tmp = in_len;
    pos 
+= 4;

    memcpy(buffer
+pos,in_buffer,in_len);

    
*out_buffer = buffer;

    
return 0;
}

 

现在还有一点,就是 这个录音机还没有加入声音的压缩,算了,等有时间再弄吧

 

这样用waveXXX函数播录放声音的用法基本上描述清楚了,不得不再次感叹,真后悔当初语文不好好学,现在连个简单

的东西都说不明白,真是抱歉,高中的时候上语文就相当于上睡觉课,不过想起来,也确实睡得很爽!

如果你觉得弄这些太没有意思,想要工程的话,那就直接留下地址吧


直接下载吧,大家看样子比较不喜欢整工程,有邮件我没有回的同志们,对不住了,工作太忙,没有维护blog了

http://git.oschina.net/linbc/WaveRecordLite.git

 

posted @ 2009-03-18 23:08  冷侃  阅读(9282)  评论(76编辑  收藏  举报