新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论高级C/C++编程、代码重构(Refactoring)、极限编程(XP)、泛型编程等话题
    [返回] 计算机科学论坛计算机技术与应用『 C/C++编程思想 』 → 利用DirectSound实现声卡录音 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 7554 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: 利用DirectSound实现声卡录音 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客楼主
    发贴心情 利用DirectSound实现声卡录音

    摘要:本文详细讲述了如何利用DirectSound对经过声卡和麦克风的数据进行捕获,进行录音,并保存为wave格式的文件。

      曾经学习过Directshow的开发,对于Dsound一直没有仔细的学习,以前只是知道Dsound是做音频开发的,我一直以为它和Dshow的结构体系差不多,经过仔细学习后,发现,其实他们完全两码事。DirectSound虽然也基于COM,但不象Dshow那样多个的filter组成链表。
    闲话少说,下面我们看看DirectSound到底能帮我们做些什么。

      1、播放WAVE格式的音频文件或者资源。

      2、可以同时播放多个音频。

      3、Assign high-priority sounds to hardware-controlled buffers

      4、播放3D立体声音

      5、在声音中添加特技效果,比如回声,动态的改变特技的参数等

      6、将麦克风或者其他音频输入设备的声音录制成wave格式的文件

      DirectSound就能做这么多事情,到这里,我都有点怀疑DirectSound是不是就是封装了mmio系列和wav系列的函数。因为这些底层的API也能够完成这些事情。这里我们主要讨论一下,如果使用Directsound进行录音,并保存成wave格式的文件。

      在开始工作之前,要先介绍DirectSound录音用到的三个非常重要的对象:

      ·IDirectSoundCapture8 ,设备对象,根据你录音的设备创建的设备对象,利用该对象可以获取设备的属性。

      ·IDirectSoundCaptureBuffer8,缓冲区对象,该对象由设备对象创建,主要用来操作音频数据

      ·IDirectSoundNotify8 ,事件通知对象,该对象用来通知应用程序从缓冲区中将数据取走,写入文件保存起来。

      利用DirectSound录音的主要思路,就是先根据选择的录音设备创建设备对象,然后通过设备对象创建辅助缓冲区对象,开始录音的时候,设备将数据写入缓冲区,应用程序主动的从缓冲区将数据读出来写文件即可,就实现了录音功能。这里简单介绍一下dsound的通知功能,应用程序会创建一个通知对象,然后将通知对象邦定,然后设定通知位置(position),什么是通知位置呢,比如缓冲区的大小为4000字节,如果你想当数据达到缓冲区一半的时候能得到通知开始copy数据,那么此时你就可以将通知位置设定为2000,通知位置可以任意的设定,当缓冲区的数据达到你设定的位置时,就会通知应用程序将缓冲区的数据copy到文件中,缓冲区是循环利用的,当缓冲区填充满了以后,就会从头开始充填数据,所以,缓冲区就是一边读,一边写的过程。

      下面我讲一下录音的主要步骤,可以使大家的思路更清晰一些

      1、枚举录音的设备

      2、根据选择的设备创建设备对象

      3、利用设备对象创建缓冲区对象

      4、设置通知机制

      5、创建工作线程,用来将缓冲区的数据写入文件。

      先来定义一下用到的数据

    LPDIRECTSOUNDCAPTURE8 g_pDSCapture = NULL;//设备对象指针
    LPDIRECTSOUNDCAPTUREBUFFER g_pDSBCapture = NULL;//缓冲区对象指针
    LPDIRECTSOUNDNOTIFY8 g_pDSNotify = NULL;//用来设置通知的对象接口

    GUID g_guidCaptureDevice = GUID_NULL; //设备id
    BOOL g_bRecording = FALSE; //是否正在录音
    WAVEFORMATEX g_wfxInput; //输入的音频格式

    DSBPOSITIONNOTIFY g_aPosNotify[ NUM_REC_NOTIFICATIONS + 1 ]; //设置通知标志的数组
    HANDLE g_hNotificationEvent; //通知事件
    BOOL g_abInputFormatSupported[20];
    DWORD g_dwCaptureBufferSize; //录音用缓冲区的大小
    DWORD g_dwNextCaptureOffset;//偏移位置
    DWORD g_dwNotifySize;// 通知位置
    CWaveFile* g_pWaveFile;//

      枚举录音的设备

      如果你的程序只是想从用户缺省的设备上进行声音的录制,那么就没有必要来枚举出系统中的所有录音的设备,当你调用DirectSoundCaptureCreate8 或者另外一个函数DirectSoundFullDuplexCreate8的时候,其实就默认指定了一个缺省的录音设备。

      当然,在下面的情况下,你就必须要枚举系统中所有的设备,例如,你的应用程序并不支持所有的输出设备,或者你的应用程需要两个或者多个设备,或者你希望用户自己来选择输出设备。

      枚举设备,你首先要定义一个回调函数,这个回调函数可以被系统中的每个设备来调用,你可以在各函数做任何事情,这个函数的命名也没有任何的限制,但是函数应该以DSEnumCallback为原型,如果枚举没有结束,这个回调函数就返回TRUE,如果枚举结束,例如你找到合适的设备,这个函数就要返回FALSE。

      下面是回调函数的一个例子,这个函数将枚举的每一个设备都添加到一个combox中,将设备的GUID保存到一个item 中,这个函数的前三个参数由设备的驱动程序提供,第四个参数有DirectSoundCaptureEnumerate函数提供,这个参数可以是任意的32位值,这个例子里是combox的句柄,

    BOOL CALLBACK DSEnumProc(LPGUID lpGUID,
    LPCTSTR lpszDesc,
    LPCTSTR lpszDrvName,
    LPVOID lpContext )
    {
    HWND hCombo = (HWND)lpContext;
    LPGUID lpTemp = NULL;

    if (lpGUID != NULL) // NULL only for "Primary Sound Driver".
    {
    if ((lpTemp = (LPGUID)malloc(sizeof(GUID))) == NULL)
    {
    return(TRUE);
    }
    memcpy(lpTemp, lpGUID, sizeof(GUID));
    }
    //下面的代码主要主要是将设备添加到CComboBox,其实你完全直接将CComboBox指针传递过来,直接的添加,这里采用的是给combox窗口发送消息的方法,
    ComboBox_AddString(hCombo, lpszDesc);
    ComboBox_SetItemData(hCombo,
    ComboBox_FindString(hCombo, 0, lpszDesc),
    lpTemp );
    free(lpTemp);
    return(TRUE);
    }

      枚举设备通常都是在对话框初始化的时候才进行的,我们假设hCombo就是combox句柄,hDlg就对话框的句柄,看看我们怎么来枚举设备的吧。

    if (FAILED(DirectSoundCaptureEnumerate ((LPDSENUMCALLBACK)DSEnumProc,
    (VOID*)&hCombo)))
    {
    EndDialog(hDlg, TRUE);
    return(TRUE);
    }

      在这个例子中,combox的句柄作为参数传递到DirectSoundEnumerate函数中,然后又被传递到回调函数中,这个参数你可以是你想传递的任意的32位值。

      注:第一个被枚举的设备通常称为Primary sound driver,并且回调函数的lpGUID为NULL,这个设备就是用户通过控制面板设置的缺省的录音声音设备,


       收藏   分享  
    顶(0)
      




    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/9/7 15:44:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客2
    发贴心情 
    创建设备对象

      你可以通过DirectSoundCaptureCreate8或者DirectSoundFullDuplexCreate8函数直接创建设备对象,该函数返回一个指向IDirectSoundCapture8接口的指针

    if( FAILED( hr = CoInitialize(NULL) ) )
    return hr;
    if(pDeviceGuid)
    {
     if(FAILED( hr = DirectSoundCaptureCreate(pDeviceGuid,&g_pDSCapture,NULL)))
      return hr;
    }
    else
    {
     if(FAILED(hr= DirectSoundCaptureCreate(&DSDEVID_DefaultCapture ,&g_pDSCapture,NULL)))
      return hr
    }

      其中pDeviceGuid是从枚举的combox中选择的设备的ID。

      现在创建了设备对象你可以通过IDirectSoundCapture8::GetCaps方法来获取录音设备的性能,这个函数的参数是一个DSCCAPS类型的结构,在传递这个参数之前,一定要初始化该结构的dwSize成员变量。同时,你可以通过这个结构返回设备支持的声道数,以及类似WAVEINCAPS结构的其他设备属性

      创建录音的缓冲区对象

      我们可以通过IDirectSoundCapture8::CreateCaptureBuffer来创建一个录音的buffer对象,这个函数的一个参数采用DSCBUFFERDESC类型的结构来说明buffer的一些特性,这个结构的最后一个成员变量是一个WAVEFORMATEX结构,这个结构一定要初始化成泥需要的wav格式。
    说明一下,如果你的应用程序一边播放的同时进行录制,如果你录制的buffer格式和你的主缓冲buffer不一样,那么你创建录制buffer对象就会失败,原因在于,有些声卡只支持一种时钟,不能同时支持录音和播放同时以两种不同的格式进行。

      下面的函数,演示了如何创建一个录音的buffer对象,这个buffer对象能够处理1秒的数据,注意,这里传递的录音设备对象参数一定要通过DirectSoundCaptureCreate8来创建,而不是早期的DirectSoundCaptureCreate接口,否则,buffer对象不支持IDirectSoundCaptureBuffer8接口。

    HRESULT CreateCaptureBuffer(LPDIRECTSOUNDCAPTURE8 pDSC,
    LPDIRECTSOUNDCAPTUREBUFFER8* ppDSCB8)
    {
     HRESULT hr;
     DSCBUFFERDESC dscbd;
     LPDIRECTSOUNDCAPTUREBUFFER pDSCB;
     WAVEFORMATEX wfx ={WAVE_FORMAT_PCM, 2, 44100, 176400, 4, 16, 0};
     // wFormatTag, nChannels, nSamplesPerSec, mAvgBytesPerSec,
     // nBlockAlign, wBitsPerSample, cbSize

     if ((NULL == pDSC) || (NULL == ppDSCB8)) return E_INVALIDARG;
     dscbd.dwSize = sizeof(DSCBUFFERDESC);
     dscbd.dwFlags = 0;
     dscbd.dwBufferBytes = wfx.nAvgBytesPerSec;
     dscbd.dwReserved = 0;
     dscbd.lpwfxFormat = &wfx; //设置录音用的wave格式
     dscbd.dwFXCount = 0;
     dscbd.lpDSCFXDesc = NULL;

     if (SUCCEEDED(hr = pDSC->CreateCaptureBuffer(&dscbd, &pDSCB, NULL)))
     {
      hr = pDSCB->QueryInterface(IID_IDirectSoundCaptureBuffer8, (LPVOID*)ppDSCB8);
      pDSCB->Release();
     }
     return hr;
    }

      你可以通过IDirectSoundCaptureBuffer8::GetCaps方法来获取录音buffer的大小,但一定要记得初始化DSCBCAPS结构类型参数的dwSize成员变量。

      为了获取buffer中数据的格式,你可以通过IDirectSoundCaptureBuffer8::GetFormat.方法来获取buffer中的数据格式,这个函数通过WAVEFORMATEX结构返回音频数据的信息,如果我们想知道一个录音buffer目前的状态如何,可以通过IDirectSoundCaptureBuffer8::GetStatus来获取,这个函数通过一个DWORD类型的参数来表示该buffer是否正在录音,
    IDirectSoundCaptureBuffer8::GetCurrentPosition方法可以获取buffer中read指针和录制指针的偏差。Read指针指向填充到该buffer中的数据的最末端,capture指针则指向复制到硬件的数据的末端,你read指针指向的前段数据都是安全数据,你都可以安全的复制。

      录音buffer对象通知机制

      为了安全的定期的从录音buffer中copy数据,你的应用程序就要知道,什么时候read指针指向了特定的位置,一个方法是通过IDirectSoundCaptureBuffer8::GetCurrentPosition.方法来获取read指针的位置,另外一个更有效的方法采用通知机制,通过IDirectSoundNotify8::SetNotificationPositions方法,你可以设置任何一个小于buffer的位置来触发一个事件,切记,当buffer正在running的时候,不要设置。

      如何来设置一个触发事件呢,首先要得到IDirectSoundNotify8接口指针,你可以通过buffer对象的QuerInterface来获取这个指针接口,对于你指定的任何一个position,你都要通过CreateEvent方法,创建一个win32内核对象, 然后将内核对象的句柄赋给DSBPOSITIONNOTIFY结构的hEventNotify成员,通过该结构的dwOffset来设置需要通知的位置在buffer中的偏移量。

      最后将这个结构或者结构数组,传递给SetNotificationPositions函数,下面的例子设置了NUM_REC_NOTIFICATIONS个通知,当position达到g_dwNotifySize时会触发一个通知,依次类推。

    HRESULT InitNotifications()
    {
     HRESULT hr ;
     g_hNotificationEvent = CreateEvent(NULL,FALSE,FALSE,NULL); //创建事件
     if(g_pDSBCapture == NULL)
      return E_FAIL;
     if(FAILED(hr = g_pDSBCapture ->QueryInterface(IID_IDirectSoundNotify,(VOID**)&g_pDSNotify)))
      return hr;

     for( INT i = 0; i < NUM_REC_NOTIFICATIONS; i++ )
     {
      g_aPosNotify[i].dwOffset = (g_dwNotifySize * i) + g_dwNotifySize - 1;
      g_aPosNotify[i].hEventNotify = g_hNotificationEvent;
     }

     if(FAILED( hr =g_pDSNotify->SetNotificationPositions( NUM_REC_NOTIFICATIONS, g_aPosNotify ) ) )
      return hr;
     return S_OK;
    }

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/9/7 15:45:00
     
     卷积内核 帅哥哟,离线,有人找我吗?
      
      
      威望:8
      头衔:总统
      等级:博士二年级(版主)
      文章:3942
      积分:27590
      门派:XML.ORG.CN
      注册:2004/7/21

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给卷积内核发送一个短消息 把卷积内核加入好友 查看卷积内核的个人资料 搜索卷积内核在『 C/C++编程思想 』的所有贴子 访问卷积内核的主页 引用回复这个贴子 回复这个贴子 查看卷积内核的博客3
    发贴心情 
    创建工作线程,用来将缓冲区的数据写入文件

    ::CreateThread(NULL,0,ThreadRecord,this,0,NULL);

      看看线程内的工作吧

    DWORD WINAPI ThreadRecord(LPVOID lpParameter)
    {
     DWORD dwResult =0;
     g_bRecording = TRUE;
     while(g_bRecording)
     {
      dwResult = WaitForMultipleObjects(1, &g_hNotificationEvent,FALSE,INFINITE );
      switch( dwResult )
      {
       case WAIT_OBJECT_0 + 0:
        RecordCapturedData();
      }
     }
     return 0;
    }

      这个线程一直在等待通知事件的触发,当缓冲区的数据填充到设定的位置时就会触发线程,这里主要有一个函数RecordCaptureedData()。这个函数主要做了下面的事情:

      1) 调用IDirectSoundCaptureBuffer8::Start使buffer对象开始工作,通过你要给这个函数dwFlags传递一个DSCBSTART_LOOPING参数,注意,buffer就会不停工作,而不是当buffer填充满了就停止工作,当缓冲区满了后,就会从头重新填充。

      2) 等待你期望的事件通知,当buffer被填充到某个你期望的位置时,会触发通知。

      3) 当你收到通知的时,你就要调用IDirectSoundCaptureBuffer8::Lock来锁住bufer的一部份,切记,不要将capture指针指向的内存锁住,你可以调用IDirectSoundCaptureBuffer8::GetCurrentPosition方法来获取read指针的位置。在传递给Lock函数的参数中,你一定要指定内存的大小和偏移量,这个函数会返回你锁住的内存的起始地址,以及block的大小。

      4) 在锁住的内存中复制data。

      5) 复制完成后要记得IDirectSoundCaptureBuffer8::Unlock方法来解锁内存。

      6)在你停止录音之前,你可以反复的重复2~5步骤,如果你想停止录音了,你可以调用IDirectSoundCaptureBuffer8::Stop方法。 将录音写入wav文件WAV文件是采用RIFF格式的文件,在文件中包含一系列的chunks,来描述头信息和数据信息,win32API提供一套mmio系列函数用来操作RIFF格式的文件,但是Directsound并没有提供读写wav格式文件的函数,但是,Directsound里封装了一个CWaveFile类用来操作wav文件,可以通过open来写入文件的头信息,write来写入文件的数据,close函数写入文件的长度,关闭文件。你可以在DirectSound的路径下找到这个类的定义(SDK root)\samples\C++\Common\Src\Dsutil.cpp。

      下面是代码,如何创建一个wav格式的文件

    CWaveFile g_pWaveFile;
    WAVEFORMATEX wfxInput;

    ZeroMemory( &wfxInput, sizeof(wfxInput));
    wfxInput.wFormatTag = WAVE_FORMAT_PCM;
    wfxInput.nSamplesPerSec = 22050
    wfxInput.wBitsPerSample = 8;
    wfxInput.nChannels = 1;
    wfxInput.nBlockAlign =
    wfxInput.nChannels * (wfxInput.wBitsPerSample / 8);
    wfxInput.nAvgBytesPerSec =
    wfxInput.nBlockAlign * wfxInput.nSamplesPerSec;

    g_pWaveFile = new CWaveFile;
    if (FAILED(g_pWaveFile->Open("mywave.wav", &wfxInput,
    WAVEFILE_WRITE)))
    {
    g_pWaveFile->Close();
    }

      下面的代码就演示了是RecordCapturedData()函数的完整定义

    HRESULT CCaptureSoundDlg::RecordCapturedData()
    {
     HRESULT hr;
     VOID *pbCaptureData = NULL;
     DWORD dwCaptureLength;
     VOID *pbCaptureData2 = NULL;
     DWORD dwCaptureLength2;
     UINT dwDataWrote;
     DWORD dwReadPos;
     DWORD dwCapturePos;
     LONG lLockSize;


     if(g_pDSBCapture == NULL )
      return S_FALSE;

     if( NULL == g_pWaveFile )
      return S_FALSE;

     if(FAILED( hr = g_pDSBCapture->GetCurrentPosition(&dwCapturePos,&dwReadPos)))
      return hr;

     lLockSize = dwReadPos -g_dwNextCaptureOffset;

     if( lLockSize < 0 )
      lLockSize += g_dwCaptureBufferSize;

     //锁住内存的大小
     //这里取模是为了使得我们读取的数据大小为g_dwNotifySize整数倍,这样buffer里剩下的也是notify的倍数
     lLockSize -= (lLockSize % g_dwNotifySize);

     if( lLockSize == 0 )
      return S_FALSE;

     //锁住内存
     if( FAILED( hr = g_pDSBCapture->Lock( g_dwNextCaptureOffset, lLockSize,
    &pbCaptureData, &dwCaptureLength,
    &pbCaptureData2, &dwCaptureLength2, 0L ) ) )
      return hr;

     // 将内存中的数据拷贝到wave文件中
     if( FAILED( hr = g_pWaveFile->Write( dwCaptureLength, (BYTE*)pbCaptureData,
    &dwDataWrote ) ) )
      return hr;

     // 移动偏移标志,循环移动
     g_dwNextCaptureOffset += dwCaptureLength;
     g_dwNextCaptureOffset %= g_dwCaptureBufferSize; // Circular buffer

     if( pbCaptureData2 != NULL )
     {
      // 将内存中的数据拷贝到wave文件中
      if( FAILED( hr = g_pWaveFile->Write( dwCaptureLength2, (BYTE*)pbCaptureData2,
    &dwDataWrote ) ) )
       return hr;

      // 移动偏移标志
      g_dwNextCaptureOffset += dwCaptureLength2;
      g_dwNextCaptureOffset %= g_dwCaptureBufferSize; // Circular buffer
     }

     //内存解锁
     g_pDSBCapture->Unlock( pbCaptureData, dwCaptureLength, pbCaptureData2, dwCaptureLength2 );

     return S_OK;
    }

      这里解释一下,IDirectSoundBuffer8::Lock可能返回两个地址的原因在于你锁定内存的数量是随机的,有时你锁定的区域正好包含buffer的起始点,这时,就会给你返回两个地址,举个例子吧。

      假设你锁定了30,000字节,偏移位置为20,000字节,也就是开始位置,如果你的缓冲区的大小为40,000字节,此时就会给你返回四个数据:

      ·内存地址的偏移位置20,000,

      ·从偏移位置到buffer的最末端的字节数,也是20,000,你要在第一个地址读取20,000个字节的内容

      ·偏移量为0的地址

      ·从起始点开始的字节数,也就是10,000字节,你要从第二个地址,也就是从0点开始读取10,000字节。

      如果不包含零点,最后两个数值返回为NULL和0,

    ----------------------------------------------
    事业是国家的,荣誉是单位的,成绩是领导的,工资是老婆的,财产是孩子的,错误是自己的。

    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2006/9/7 15:46:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 C/C++编程思想 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/11/22 20:47:55

    本主题贴数3,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    93.750ms