Directsound开发指南(3)

时间:2021-11-14 03:20:46

3.3Using WAV Data

  WDM驱动模式下,DirectSound缓冲区支持如下WAV格式:多声道,多个扬声器配置,例如5.1,在前左,前中,前右,后左,后右都有扬声器,超重低音。也支持多于16的采样精度。

  这种格式可以用WAVEFORMATEXTENSIBLE结构来描述,这个结构是WAVEFORMATEX的扩展,

  对于多声道,DirectSound并不支持3D

 

 

 

  下面我讲一下如何计算wave的播放时间

  Wave格式的播放长度由数据大小和格式决定,可以通过CWaveFile::GetSizeCWaveFile::GetFormat来获取数据的大小和格式

下面的代码告诉你如何计算wave的总播放时间以毫秒为时间单位

DWORD GetSoundLength(LPSTR strFileName)

{

  CWaveFile* pWav;

  DWORD dwLen = 0;

  DWORD dwSize;

  WAVEFORMATEX* wfx;

 

  pWav = new CWaveFile();

  if (SUCCEEDED(pWav->Open(strFileName, NULL, WAVEFILE_READ)))

  {

    wfx = pWav->GetFormat();

    dwSize = pWav->GetSize();

    dwLen = (DWORD) (1000 * dwSize / wfx->nAvgBytesPerSec);

    pWav->Close();

  }

  if (pWav) delete pWav;

  return dwLen;

}

 

 

 

3.43-D Sound

 

 

 

3.5增加声音特技Using Effects

   DirectX通过DirectX  Media ObjectsDMOs)来支持在声音中添加特技。

为了使用特技,DirectSound 应用程序必须首先CoInitialize来初始化Com库,当然也不排除通过DirectSoundCreate8来创建设备对象。

  对于在DSBUFFERDESC结构中设置DSBCAPS_CTRLFX标志的辅助缓冲区,你可以在该缓冲区添加任意的特技,但是,buffer一定要处于停止状态并且没有被锁定,在DiectX 9.0中,特技不依靠硬件来加速。当然也可以在硬件缓冲区中设置特技,但这样做没有任何的好处。

  对于很小的缓冲区,特技也许并不能好好工作,DirectSound不提倡在小于150毫秒的数据缓冲区中使用特技,这个值被定义为DSBSIZ_FX_MIN

   下面的代码在缓冲设置了回声特技。

HRESULT SetEcho(LPDIRECTSOUNDBUFFER8 pDSBuffer)

{

  HRESULT hr;

  DWORD dwResults[1];  // One element for each effect.

 

  // Describe the effect.

  DSEFFECTDESC dsEffect;

  memset(&dsEffect, 0, sizeof(DSEFFECTDESC));

  dsEffect.dwSize = sizeof(DSEFFECTDESC);

  dsEffect.dwFlags = 0;

  dsEffect.guidDSFXClass = GUID_DSFX_STANDARD_ECHO; //设置特技

 

  // Set the effect

  if (SUCCEEDED(hr = pDSBuffer->SetFX(1, &dsEffect, dwResults)))

  {

    switch (dwResults[0])

    {

      case DSFXR_LOCHARDWARE:

        OutputDebugString("Effect was placed in hardware.");

        break;

      case DSFXR_LOCSOFTWARE:

        OutputDebugString("Effect was placed in software.");

        break;

      case DSFXR_UNALLOCATED:

        OutputDebugString("Effect is not yet allocated to hardware or software.");

        break;

    }

  }

  return hr;

}

 

 

 

 为了获取或者设置特技的参数,首先你要从包含特技的缓冲区对象中获取相应的特技接口指针。下面列出DirectSound支持的特技接口。

下面我简单介绍一下标准的特技

Chorus

Compression

Distortion

Echo

Environmental Reverberation

Flange

Gargle

Parametric Equalizer

Waves Reverberation

3.6录制Capturing Waveforms

1枚举录音的设备

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

 DirectSoundFullDuplexCreate8的时候,其实就默认指定了一个缺省的录音设备。

当然,在下面的情况下,你就必须要枚举系统中所有的设备,

DWORD pv;  // Can be any 32-bit type.

 

HRESULT hr = DirectSoundCaptureEnumerate(

    (LPDSENUMCALLBACK)DSEnumProc, (VOID*)&pv);

2创建设备对象

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

 

 

 

3录音设备的性能

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

4创建录音的buffer

我们可以通过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;

  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;

}

5通过录音buffer对象获取信息

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

为了获取buffer中数据的格式,你可以通过IDirectSoundCaptureBuffer8::GetFormat.方法来获取buffer中的数据格式,这个函数通过WAVEFORMATEX结构返回音频数据的信息,

 如果我们想知道一个录音buffer目前的状态如何,可以通过IDirectSoundCaptureBuffer8::GetStatus来获取,这个函数通过一个DWORD类型的参数来表示该buffer是否正在录音,

IDirectSoundCaptureBuffer8::GetCurrentPosition方法可以获取bufferread指针和录制指针的偏差。Read指针指向填充到该buffer中的数据的最末端,capture指针则指向复制到硬件的数据的末端,你read指针指向的前段数据都是安全数据,你都可以安全的复制。

6录音buffer对象通知机制

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

来触发一个事件,切记,当buffer正在running的时候,不要设置。

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

最后将这个结构或者结构数组,传递给SetNotificationPositions函数,下面的例子设置了三个通知,当position达到buffer的一半时会触发一个通知,当达到buffer的末端时,触发第二个通知,当录音停止时触发第三个通知消息。

HRESULT SetCaptureNotifications(LPDIRECTSOUNDCAPTUREBUFFER8 pDSCB)

{

  #define cEvents  3

 

 

 

  LPDIRECTSOUNDNOTIFY8 pDSNotify;

  WAVEFORMATEX         wfx; 

  HANDLE     rghEvent[cEvents] = {0};

  DSBPOSITIONNOTIFY  rgdsbpn[cEvents];

  HRESULT    hr;

 

 

 

  if (NULL == pDSCB) return E_INVALIDARG;

  if (FAILED(hr = pDSCB->QueryInterface(IID_IDirectSoundNotify, (LPVOID*)&pDSNotify)))

  {

    return hr;

  }

  if (FAILED(hr = pDSCB->GetFormat(&wfx, sizeof(WAVEFORMATEX), NULL)))

  {

    return hr;

  }

 

 

 

  // Create events.

  for (int i = 0; i < cEvents; ++i)

  {

    rghEvent[i] = CreateEvent(NULL, TRUE, FALSE, NULL);

    if (NULL == rghEvent[i])

    {

      hr = GetLastError();

      return hr;

    }

  }

 

  // Describe notifications.

 

  rgdsbpn[0].dwOffset = (wfx.nAvgBytesPerSec/2) -1;

  rgdsbpn[0].hEventNotify = rghEvent[0];

 

  rgdsbpn[1].dwOffset = wfx.nAvgBytesPerSec - 1;

  rgdsbpn[1].hEventNotify = rghEvent[1];

 

  rgdsbpn[2].dwOffset = DSBPN_OFFSETSTOP;

  rgdsbpn[2].hEventNotify = rghEvent[2];

 

  // Create notifications.

 

  hr = pDSNotify->SetNotificationPositions(cEvents, rgdsbpn);

  pDSNotify->Release();

  return hr;

}

7录音的步骤

录音主要包括下面几个步骤

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方法。

 

 

 

9将录音写入wav文件

WAV文件是采用RIFF格式的文件,在文件中包含一系列的chunks,来描述头信息和数据信息,win32API提供一套mmio系列函数用来操作RIFF格式的文件,但是Directsound并没有提供读写wav格式文件的函数,但是,Directsound里封装了一个CWaveFile类用来操作wav文件,可以通过open来写入文件的头信息,write来写入文件的数据,close函数写入文件的长度,关闭文件。

下面是代码,如何创建一个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();

}

下面的代码就演示了如何从buffer对象中将数据写入文件中

HRESULT RecordCapturedData()

{

  HRESULT hr;

  VOID* pbCaptureData  = NULL;

  DWORD dwCaptureLength;

  VOID* pbCaptureData2 = NULL;

  DWORD dwCaptureLength2;

  VOID* pbPlayData   = NULL;

  UINT  dwDataWrote;

  DWORD dwReadPos;

  LONG lLockSize;

 

  if (NULL == g_pDSBCapture)

      return S_FALSE;

  if (NULL == g_pWaveFile)

      return S_FALSE;

 

  if (FAILED (hr = g_pDSBCapture->GetCurrentPosition(

    NULL, &dwReadPos)))

      return hr;

 

  // Lock everything between the private cursor

  // and the read cursor, allowing for wraparound.

 

  lLockSize = dwReadPos - g_dwNextCaptureOffset;

  if( lLockSize < 0 ) lLockSize += g_dwCaptureBufferSize;

 

  if( lLockSize == 0 ) return S_FALSE;

 

  if (FAILED(hr = g_pDSBCapture->Lock(

        g_dwNextCaptureOffset, lLockSize,

        &pbCaptureData, &dwCaptureLength,

        &pbCaptureData2, &dwCaptureLength2, 0L)))

    return hr;

 

  // Write the data. This is done in two steps

  // to account for wraparound.

 

 

 

  if (FAILED( hr = g_pWaveFile->Write( dwCaptureLength,

      (BYTE*)pbCaptureData, &dwDataWrote)))

    return hr;

 

  if (pbCaptureData2 != NULL)

  {

    if (FAILED(hr = g_pWaveFile->Write(

        dwCaptureLength2, (BYTE*)pbCaptureData2,

        &dwDataWrote)))

      return hr;

  }

 

  // Unlock the capture buffer.

 

 g_pDSBCapture->Unlock( pbCaptureData, dwCaptureLength,

    pbCaptureData2, dwCaptureLength2  );

 

  // Move the capture offset forward.

 

  g_dwNextCaptureOffset += dwCaptureLength;

  g_dwNextCaptureOffset %= g_dwCaptureBufferSize;

  g_dwNextCaptureOffset += dwCaptureLength2;

  g_dwNextCaptureOffset %= g_dwCaptureBufferSize;

 

  return S_OK;

}