FreeEIM 局域网聊天软件

局域网聊天工具,文字讯息、文件发送、语音通讯、高清视频通讯、远程桌面控制。

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  3 随笔 :: 2 文章 :: 0 评论 :: 0 Trackbacks

2008年1月18日 #

     摘要: This article demonstrates how to generate a quasi-infinite number of sounds using math formulas. Also, a sample program is provided to allow you to do experimentation by your own. Have great times!  阅读全文
posted @ 2008-01-18 11:25 FEIM Studios 阅读(40) | 评论 (0)编辑 收藏

摘要:本文详细讲述了如何利用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,这个设备就是用户通过控制面板设置的缺省的录音声音设备,创建设备对象

  你可以通过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;
  } 

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

  ::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,

  关于DirectSound对声卡录音就简单介绍介绍到这里,留下msn :aooang@hotmail.com,欢迎和大家交流,我在天机的blog, 里面有关于DirectSound和DirectShow的不少资料,我翻译了DirectShow和DirectSound的SDK文档,欢迎来信索取,共同学习。

posted @ 2008-01-18 11:22 FEIM Studios 阅读(61) | 评论 (0)编辑 收藏

目录:
  关于DirectSound
  DirectSound设备
    枚举可以使用的声音设备
    创造DirectSound对象
    设置合作级
    检索硬件信息
    扬声器的设置
    压缩
  DirectSound缓冲
    静态缓冲和流缓冲
    创建辅助缓冲
    缓冲控制选项
    主缓冲的存取
    播放声音
    重放(PLAYBACK)的控制
    播放进度和可以被写的位置(Current Play and Write Positions)
    播放缓冲时的通知(PLAY BUFFER NOTIFITACION)
    混音(MIXING SOUND)
    自己的混音

文档内容:


(费话篇)
关于DirectSound


  DirectSound是DirectX API的音频(waveaudio)组件之一,它可以提供快速的混音、硬件加速功能,并且可以直接访问相关设备,当然,最主要的是它提供的功能与现有的(?将来的呢?)设备驱动程序保持兼容性。
DirectSound允许进行波型声音的捕获、重放,也可以通过控制硬件和相应的驱动来获得更多的服务。
DirectSound的优势当然和DirectX的其它组件一样——速度,它允许你最大效率的使用硬件,并拥有良好的兼容性(其实别的都好说,就要这两样就够了:P)。

使用DirectSound可以做到什么呢?

1、很方便的了解硬件能力,并且根据当前计算机硬件配置硬件来决定最好的解决问题的方法。

2、弥补驱动程序的不足——通过属性设置以便硬件能力可以完全发挥,即便是驱动程序没有很好的支持该功能。

3、短传输延迟时间的混音为了快速的响应流。

4、3-D声音

5、声音的捕获

 

DirectSound核心
DirectSound设备

 

  这部分描述了将怎样枚举可以使用的声音设备、为一设备创造DirectSound对象、使用对象的方法来设置合作级别、检验设备的能力、创造声音缓冲、配置系统扬声器和压缩数据。

枚举可以使用的声音设备

  一般的情况下,可能并不需要枚举可以使用的声音设备,使用缺省设备不会带来什么麻烦(相信用户-上帝:P的设置),但是如果你希望将程序做得更“面对对象”一些,给用户更多更好的选择(很多3D GAME都可以让用户来选择显卡,道理大同小异),那你就需要使用到枚举可供使用的声音设备了。
  枚举可供使用的声音设备首先要写一回调函数,在程序每找到一可供使用的声音设备时调用,在那个函数里你可以做任何事,并且它可以拥有任何的合法的名字,但是你必须作为DSEnumCallback声明它,回调函数必须返回一BOOL,TRUE则继续列表,FALSE就退出枚举过程了。
  和显示设备的枚举差不多,下面就是载自Dsenum.c的几段代码。由于本人认为使用得不多就没怎么研究(其实也是很简单的:P),所以就不再累赘,后面还有更令人兴奋的东西呢!

代码如下:

//回调函数
BOOL CALLBACK DSEnumProc(LPGUID lpGUID,LPCTSTR lpszDesc,LPCTSTR lpszDrvName,LPVOID lpContext )
{
    HWND hCombo = *(HWND *)lpContext;
    LPGUID lpTemp = NULL;

    if ( lpGUID != NULL )
    {
        if (( lpTemp = malloc( sizeof(GUID))) == NULL )
            return( TRUE );

        memcpy( lpTemp, lpGUID, sizeof(GUID));
    }

    ComboBox_AddString( hCombo, lpszDesc );
    ComboBox_SetItemData( hCombo,
    ComboBox_FindString( hCombo, 0, lpszDesc ), lpTemp );  //编辑者注:此句有误
    return( TRUE );
}

创造DirectSound对象

创造DirectSound对象最简单的方法是调用DirectSoundCreate函数。

LPDIRECTSOUND lpds;
HRESULT hr = DirectSoundCreate(NULL, &lpds, NULL));

该函数的第一个参数是硬件设备,NULL表示使用默认的设备,第二个参数是远程指针LPDIRECTSOUND的地址,也就是创造的DirectSound对象放置的地址,第三个参数必须为NULL,暂时没有用。
当没有相应的设备或设备在别的程序的控制下不能响应你的呼叫时,函数返回出错,这时,如果你的程序继续工作,所有和DirectSound对象相关的操作都将不可进行!

设置合作级

因为WINDOWS是一多任务环境,可以允许多个应用程序同时工作,当然也会产生多个程序在同时里使用同一设备工作的情况,通过合作级别,DirectX可以保证所有的程序在使用同一设备时不会发生冲突(大家和平共处岂不是一件乐事),所以每个使用DirectSound的程序都应该有一合作级别用来决定允许访问的设备。
DirectSound有四种合作级别:标准级、优先级、独占级和写主缓冲级(write-primary,写是主要的动作),其中游戏普遍使用优先级这种级别可以使程序在同一采样条件下作出最柔韧的输出(MS的文档也很有文学味的嘛!)。

//C的例子,会换成C++的吗?不会说NO吧!
HRESULT hr = lpDirectSound->lpVtbl->SetCooperativeLevel( lpDirectSound, hwnd, DSSCL_PRIORITY );

标准级(DSSCL_NORMAL):该级别只能使用22KHZ、立体声(STEREO)、8位的音乐,并且不能直接的写主缓冲,也不能使用压缩过的声音。

优先级(DSSCL_PAIORITY):可以实现硬件混合(hardware mixing),可以设置主缓冲的声音格式(可以根据需要来使用不同质量的音乐)和压缩过的音乐。

独占级(DSSCL_EXCLUSIVE):当应用程序在前台工作时,其它程序是不可使用声音的。

写主缓冲级(DSSCL_WRITEPRIMARY):最高的合作级,程序可以直接的操纵主缓冲,而且程序必须直接的写主缓冲区(最基层的操作)。在这种级别,第二缓冲将不可用。
除了该级别外,所有试图LOCK主缓冲的操作都将失败,也就是说只有该级别可以对主缓冲进行写操作!
当使用写主缓冲级的程序处于前台时,后台所有程序的第二缓冲都将停止且丢失,而如果这时使用写主缓冲级的程序转到后台工作,它的主缓冲也将丢失并且在又一次转到前台工作时应该还原(restore)。更多的信息将在缓冲区管理里阐述(现在还没有翻译:P不过用过了DDraw以后这些东西应该很熟悉了才是)。
如果要设置写主缓冲级,先应该确定现在是否可以使用该级别——使用IDirectSound::GetCaps函数,检查DSCAPS结构里是否有DSCAPS_EMULDRIVER标志。

检索硬件信息

DirectSound允许应用程序检索硬件信息,当然,在一般情况下,这样做是不必要的,因为DirectSound可以自动有效的使用硬件加速,我们完全可以不用去管硬件是否具有某些能力,不过为了提高程序的效率,这样做还是是有用处的。
检索硬件信息使用IDirectSound::GetCaps方法,例如:

DSCAPS dscaps;

dscaps.dwSize = sizeof(DSCAPS);
HRESULT hr = lpDirectSound->lpVtbl->GetCaps(lpDirectSound,&dscaps);

扬声器的设置

DirectSound的扬声器设置可以用来调节输出音量的大小和优化3D效果。
在WIN98和WIN2000,可以通过IDirectSound::GetSpeakerConfig来获得当前扬声器的设置,并通过IDirectSound::SetSpeakerConfig来改变扬声器设置;而在WIN95里IDirectSound::GetSpeakerConfig只是简单的返回一默认值或是返回使用IDirectSound::SetSpeakerConfig设置后的值。

压缩

应用程序可以通过IDirectSound::Compact来获得最多的可用内存,当然从前面对合作级别的讨论我们可以发现使用压缩的前提是程序的合作级别至少应该是优先级。

 

DirectSound缓冲
基础(又是费话连篇)


  在初始化DirectSound时,它会自动地为你的程序创建一主缓冲,这个主缓冲的作用就是混音并送到输出设备。
除了主缓冲外,程序至少还应该创建一个辅助缓冲,辅助缓冲的作用是储存将要使用的声音,它可以在不使用的时候释放掉(费话,不过这也就是暗示我们主缓冲是不可释放的)。

你可以在同一段物理内存上创建两个或更多的辅助缓冲(使用IDirectSound::DuplicateSoundBuffer方法),但是如果最初的缓冲(原本)所在的硬件内存资源不支持多缓冲,那么这个调用将以失败告终。
DirectSound还可以同时播放多个声音,当然其大前提是硬件允许。
DirectSound播放一声音只需要很短的时间延迟,如果在播放声音的同时来播放动画,你将感觉不到延迟,但是如果DirectSound需要通过软件仿真来完成这一动作,那么延迟时间将延长5-8倍。
通常情况下,你并不需要和主缓冲打交道,DirectSound会自己来管理它的,除非你要使用自己写的混音部分,这时,DirectSound就会让你自行管理主缓冲,更详细的讲解见主缓冲的存取部分。

静态缓冲和流缓冲

在应用程序里,辅助缓冲可以有两种——静态缓冲(一段内存空间一段完整的声音;好处在于可以一次将全部的声音存入缓冲)和流缓冲(并不将全部的数据一次读入缓冲,而是在播放声音时动态的读入;其好处在于占用空间较小),它们可以分别适应不同的程序需求。
一般的说,如果声音是需要再三播放的,而且容量有限(好比游戏音效),那么使用静态缓冲就更有助于提高程序的效率,相反,如果是很冗长的音乐,还是使用流缓冲的好。

创建辅助缓冲

//创建辅助缓冲的例子
BOOL AppCreateBasicBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb )
{
    PCMWAVEFORMAT pcmwf;
    DSBUFFERDESC dsbdesc;
    HRESULT hr;

    //设置PCMWAVEFORMAT结构
    memset(&pcmwf, 0, sizeof(PCMWAVEFORMAT));
    pcmwf.wf.wFormatTag = WAVE_FORMAT_PCM;
    pcmwf.wf.nChannels = 2;
    pcmwf.wf.nSamplesPerSec = 22050;
    pcmwf.wf.nBlockAlign = 4;
    pcmwf.wf.nAvgBytesPerSec =
    pcmwf.wf.nSamplesPerSec * pcmwf.wf.nBlockAlign;
    pcmwf.wBitsPerSample = 16;

    //设置DSBUFFERDESC结构
    memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); //将结构置0.
    dsbdesc.dwSize = sizeof(DSBUFFERDESC);
    //使用默认的设置(音量之类)
    dsbdesc.dwFlags = DSBCAPS_CTRLDEFAULT;
    //3秒钟长度的缓冲(3-second buffer)
    dsbdesc.dwBufferBytes = 3 * pcmwf.wf.nAvgBytesPerSec;
    dsbdesc.lpwfxFormat = (LPWAVEFORMATEX)&pcmwf;
    //创建缓冲
    hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound,
    &dsbdesc, lplpDsb, NULL);
    if SUCCEEDED(hr)
    {
        //成功
        return TRUE;
    }
    else
    {
        //失败
        *lplpDsb = NULL;
        return FALSE;
    }
}

很简单是吧,只要填两个STRUCT就OK了。

因为DirectSound对先创建的缓冲优先分配硬件资源,所以你应该先创建重要的缓冲。如果你事先声明要创建一硬件缓冲(和放在显存里的表面差不多),就应该在DSBUFFERDESC结构里设置DSBCAPS_LOCHARDWARE标志,但是如果你得不到足够的硬件资源(硬件内存或混音容量hardware memory or mixing capacity),将无法创建缓冲。
创建缓冲时,也可以声明是静态缓冲(设置DSBCAPS_STATIC标志)还是流缓冲;默认值是流缓冲(上面就是使用的默认值)。
缓冲是和DirectSound对象相关联的,如果释放了DirectSound对象,则它所有的缓冲也都将被释放。

缓冲控制选项

你创建一辅助缓冲时,还应该声明该缓冲需要用到的控制选项。这项工作需要你为DSBUFFERDESC结构设置以DSBCAPS_CTRL为首的标志(这些标志可以是单独的来使用,也可以同时设置几个)。
可用的控制有3-D属性、频率、Pan(左右正道的差值)、音量、Position notification(可能是指播放时的进度)。
为了能在所有的声卡上都可以获得做好的效果,最好只设置需要的控制选项。如果一块声卡支持硬件缓冲但不支持底盘控制(pan control),那么DiractSound只会在DSBCAPS_CTRLPAN标志没有被声明时使用硬件加速。这也就是说,DirectSound通过控制选项来决定如何为缓冲来分配硬件资源。
如果你使用一个缓冲不支持的控制,譬如为一个并没有声明DSBCAPS_CTRLVOLUME标志的缓冲调用IDirectSoundBuffer::SetVolume方法,是不可能成功的。

主缓冲的存取

如果你不满意DirectSound的工作,可以直接的操纵主声音缓冲,也可以说是直接的操纵硬件了,但是这将意味着DirectSound的部分特性不可用,包括辅助缓冲的混音和混音的硬件加速。
主缓冲其实是硬件缓冲,它的大小是由硬件来决定的,而这个值通常是很小的,因此你应该使用数据流的方式来访问该缓冲。而且如果硬件不提供主缓冲,你就不能直接的访问它了(其实是访问DX软件仿真的主缓冲);你应该调用IDirectSoundBuffer::GetCaps方法来检查DSBCAPS结构里是否有DSBCAPS_LOCHARDWARE标志,有才可以设置DSSCL_WRITEPRIMARY合作级别来访问主缓冲。

//写主缓冲时的初始化工作
BOOL AppCreateWritePrimaryBuffer( LPDIRECTSOUND lpDirectSound, LPDIRECTSOUNDBUFFER *lplpDsb, LPDWORD lpdwBufferSize, HWND hwnd )
{
    DSBUFFERDESC dsbdesc;
    DSBCAPS dsbcaps;
    HRESULT hr;
    WAVEFORMATEX wf;

    //初始化WAVEFORMATEX 结构
    memset(&wf, 0, sizeof(WAVEFORMATEX));
    wf.wFormatTag = WAVE_FORMAT_PCM;
    wf.nChannels = 2;
    wf.nSamplesPerSec = 22050;
    wf.nBlockAlign = 4;
    wf.nAvgBytesPerSec =
    wf.nSamplesPerSec * wf.nBlockAlign;
    wf.wBitsPerSample = 16;

    //初始化DSBUFFERDESC结构
    memset(&dsbdesc, 0, sizeof(DSBUFFERDESC));
    dsbdesc.dwSize = sizeof(DSBUFFERDESC);
    dsbdesc.dwFlags = DSBCAPS_PRIMARYBUFFER;
    //缓冲的大小是由硬件决定的
    dsbdesc.dwBufferBytes = 0;
    dsbdesc.lpwfxFormat = NULL; //该字段必须置NULL

    //设置合作级别
    hr = lpDirectSound->lpVtbl->SetCooperativeLevel(lpDirectSound,
    hwnd, DSSCL_WRITEPRIMARY);
    if SUCCEEDED(hr)
    {
        //创建缓冲
        hr = lpDirectSound->lpVtbl->CreateSoundBuffer(lpDirectSound, &dsbdesc, lplpDsb, NULL);
        if SUCCEEDED(hr)
        {
            //设置主缓冲需要的格式
            hr = (*lplpDsb)->lpVtbl->SetFormat(*lplpDsb, &wf);
            if SUCCEEDED(hr)
            {
                //获得主缓冲的大小
                dsbcaps.dwSize = sizeof(DSBCAPS);
                (*lplpDsb)->lpVtbl->GetCaps(*lplpDsb, &dsbcaps);
                *lpdwBufferSize = dsbcaps.dwBufferBytes;
                return TRUE;
            }
        }
    }

    //如果失败
    *lplpDsb = NULL;
    *lpdwBufferSize = 0;
    return FALSE;
}

播放声音

播放声音要通过以下步骤:
1、锁定辅助缓冲的一部分以获得你所需要的那部分缓冲的基址。
2、向缓冲写数据。
3、解锁。
4、使用IDirectSoundBuffer::Play方法来播放声音。
如果是使用的流缓冲,还需要反复的执行1-3步骤。

因为流缓冲存储通常是循环的(就像循环队列),所以当你锁定缓冲时DirectSound会返回2个指针。譬如你从一个只有4,000字节的缓冲中点开始锁定3,000字节长的数据,那么DirectSound返回的第一个指针是从中点开始的那2,000字节,而第二个指针则是缓冲最前面的那1,000字节。当然如果没有发生这种情况第二个指针是NULL。
如果你设置了DSBPLAY_LOOPING标志,那么音乐将不停的播放下去,除非你使用IDirectSoundBuffer::Stop来停止它。
有关流缓冲的部分在后继章节里还将详细的讨论到。

下面就是一个C语言的例子:
//写辅助缓冲
BOOL AppWriteDataToBuffer(
LPDIRECTSOUNDBUFFER lpDsb, //缓冲
DWORD dwOffset,            //要写入数据的缓冲徧移地址
LPBYTE lpbSoundData,       //要写入的数据
DWORD dwSoundBytes)        //一次写入的块的大小
{
    LPVOID lpvPtr1;
    DWORD dwBytes1;
    LPVOID lpvPtr2;
    DWORD dwBytes2;
    HRESULT hr;

    //获得将要写的块的地址
    hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0);

    //如果返回DSERR_BUFFERLOST,还原并重新锁定
    if (DSERR_BUFFERLOST == hr)
    {
        lpDsb->lpVtbl->Restore(lpDsb);
        hr = lpDsb->lpVtbl->Lock(lpDsb, dwOffset, dwSoundBytes, &lpvPtr1, &dwAudio1, &lpvPtr2, &dwAudio2, 0);
    }

    if SUCCEEDED(hr)
    {
        //拷贝数据
        CopyMemory(lpvPtr1, lpbSoundData, dwBytes1);
        if (NULL != lpvPtr2)
        {
            CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2);
        }
        //解锁
        hr = lpDsb->lpVtbl->Unlock(lpDsb, lpvPtr1, dwBytes1, lpvPtr2, dwBytes2);
        if SUCCEEDED(hr)
        {
            //成功
            return TRUE;
        }
    }

    //失败
    return FALSE;
}

重放(PLAYBACK)的控制

你可以通过IDirectSoundBuffer::GetVolume和IDirectSoundBuffer::SetVolume来获得或设置该缓冲的音量,设置主缓冲的音量将改变声卡的设置。
同样的,你也可以通过IDirectSoundBuffer::GetFrequency和IDirectSoundBuffer::SetFrequency来获得或设置声音的频率,通过IDirectSoundBuffer::GetPan和IDirectSoundBuffer::SetPan来检索或改变左右声道的相对差,但是你不可以改变主缓冲的相应设置。

诚如前面所说的,这些缓冲控制都必须在设置了相应的标志才可以使用。

播放进度和可以被写的位置(Current Play and Write Positions)

DirectSound通常都保证缓冲里有两个指针,一个是当前的播放位置——即当前的播放进度,一个是当前的可以写数据的位置。这两个指针都只是相对缓冲而言的偏移而已。

IDirectSoundBuffer::Play方法通常都从当前的播放进度开始播放音乐。在缓冲刚建立时,播放进度是指向0,而当一段音乐播放完毕以后,播放进度指向那段音乐数据最末端的下一字节,同样的,当音乐被停止时,播放进度也指向停止位置的下一字节。

我们可以将缓冲想象成一个时钟的钟面,而这两个指针则可以作为是钟面上的两个指针。如果数据是顺时针的写上去的,那么可以被写数据的位置始终在当前的播放进度的前面——如果当前的播放进度是1,那么从2开始才是可以写数据的位置;而当播放进度到2这个位置时,从3开始才是现在可以写数据的位置了。

需要注意的是如果你使用的是流缓冲,那么你应该自己来维护现在可以写数据的位置,而且这个指针和IDirectSoundBuffer::Lock里的那个参数dwWriteCursor不是一回事,那个参数只是你想从什么位置开始写你的数据(记住是你想而不是你只能)。当然你也可以在dwFlags参数里加上DSBLOCK_FROMWRITECURSOR标志来使函数忽略dwWriteCursor参数而从当前可以写数据的那个位置开始写数据。

你可以通过IDirectSoundBuffer::GetCurrentPosition和IDirectSoundBuffer::SetCurrentPosition来检索或设置这两个指针,不过当前可以写数据的位置是不可以由你自己决定的,而应该在创建缓冲时加入DSBCAPS_GETCURRENTPOSITION2标志来保证当前可以写数据的位置是正确的。

播放缓冲时的通知(PLAY BUFFER NOTIFITACION)

在你使用流缓冲时,很可能需要知道播放进度已经到什么位置了,或者重放被停止没有。你可以通过IDirectSoundNotify::SetNotificationPositions方法来在缓冲里设置若干个通知点,当相应的事件在这些点发生时DirectSound会给予通知。但是如果音乐已经在播放了,是不允许做这些事的。

首先你应该获得IDirectSoundNotify接口的指针,就像下面一样:

// LPDIRECTSOUNDBUFFER lpDsbSecondary;
// 缓冲已经被初始化

LPDIRECTSOUNDNOTIFY lpDsNotify; //接口指针

HRESULT hr = lpDsbSecondary->QueryInterface(IID_IDirectSoundNotify, (LPVOID *)&lpDsNotify);
if SUCCEEDED(hr)
{
    //成功后就可以使用lpDsNotify->SetNotificationPositions了。
}

注意:IDirectSoundNotify接口和创建它的辅助缓冲是相关联的。

现在你可以通过WIN32 API的CreateEvent()来创建一事件对象。然后你需要为DSBPOSITIONNOTIFY结构的hEventNotify设置一句柄(CreateEvent()返回的),并且设置你想设置的通知位置的偏移值给dwOffset,就可以来设置通知位置了。

设置通知位置的例子如下:

DSBPOSITIONNOTIFY PositionNotify;

PositionNotify.Offset = DSBPN_OFFSETSTOP;
PositionNotify.hEventNotify = hMyEvent;
// hMyEvent是一个由CreateEvent()返回的句柄

lpDsNotify->SetNotificationPositions(1, &PositionNotify);

如果你需要设置更多的通知位置,你可以通过结构数组来实现。

混音(MIXING SOUND)

对DirectSound来说混音是很容易的,它允许你同时播放多个辅助缓冲,它可以自己来完成这些任务。

只要你的程序正确的指定DSBCAPS_STATIC标志,DirectSound就可以最大限度的使用硬件加速,这些标志需要在静态缓冲重新使用时再指定一次。

如果你所有的缓冲都使用同一种声音格式而且硬件输出也是使用这种格式,那么DirectSound的混音将不需要在格式转换上花任何的工夫,从而大到最优的效果(什么都是最优!:P)。

我们可以通过创建一主缓冲或是调用IDirectSoundBuffer::SetFormat方法来改变硬件输出格式,记住这主缓冲仅仅是为控制目的,和写主缓冲是不一样的,而且这种调用必须要DSSCL_PRIORITY(优先级)或更高的级别。

自己的混音

只有在DSSCL_WRITEPRIMARY级别才可以使用自己写的混音部分。在设置了合作级别后,创建主缓冲,然后锁定它,并写数据,再就可以像其它的缓冲一样的来播放了,不过需要设置DSBPLAY_LOOPING标志才可以。

下面就是一个例子:

BOOL AppMixIntoPrimaryBuffer( LPAPPSTREAMINFO lpAppStreamInfo, LPDIRECTSOUNDBUFFER lpDsbPrimary, DWORD dwDataBytes, DWORD dwOldPos, LPDWORD lpdwNewPos)
{
    LPVOID lpvPtr1;
    DWORD dwBytes1;
    LPVOID lpvPtr2;
    DWORD dwBytes2;
    HRESULT hr;

    //锁定缓冲
    hr = lpDsbPrimary->lpVtbl->Lock(lpDsbPrimary, dwOldPos, dwDataBytes, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0);

    //如果返回DSERR_BUFFERLOST,还原DS并从新锁定
    if (DSERR_BUFFERLOST == hr)
    {
        lpDsbPrimary->lpVtbl->Restore(lpDsbPrimary);
        hr = lpDsbPrimary->lpVtbl->Lock(lpDsbPrimary, dwOldPos, dwDataBytes, &lpvPtr1, &dwBytes1, &lpvPtr2, &dwBytes2, 0);
    }

    if SUCCEEDED(hr)
    {
        //将混音的数据送到缓冲区内
        CustomMixer(lpAppStreamInfo, lpvPtr1, dwBytes1);
        //该函数负责混合若干数据流。下同
        *lpdwNewPos = dwOldPos + dwBytes1;
        if (NULL != lpvPtr2)
        {
            CustomMixer(lpAppStreamInfo, lpvPtr2, dwBytes2);
            *lpdwNewPos = dwBytes2;
        }

        //解锁
        hr = lpDsbPrimary->lpVtbl->Unlock(lpDsbPrimary, lpvPtr1, dwBytes1, lpvPtr2, dwBytes2);

        if SUCCEEDED(hr)
        {
            return TRUE;
        }
    }

    //锁定或解锁失败
    return FALSE;
}

posted @ 2008-01-18 10:32 FEIM Studios 阅读(44) | 评论 (0)编辑 收藏

仅列出标题