Directsound开发指南(2)

时间:2021-11-14 03:26:10

3.2Dsoundbuffer对象(DirectSound Buffers)

  在存储和播放几个音频流的时候,你的应用程序要给每一个音频流都要创建一个辅助缓冲区(buffer)对象。

  辅助缓冲区可以和应用程的生命期一样的长,也可以在不需要的时候销毁。辅助缓冲区可以是一个包含了整个声音数据的静态缓冲区,也是可以只包含声音数据的一部份,然后再播放时不断更新数据的流缓冲区。为了限制内存开销,在播放比较长的声音文件时要采用流缓冲区,这些缓冲区只包含几秒钟的数据量。

  你可以通过同时播放几个辅助缓冲区中的声音来对他们进行混音,至于同时可以播放几个辅助缓冲区则有硬件设备的性能决定。

辅助缓冲区的格式并不完全一样,一般用来描述缓冲格式的参数如下

 Format,缓冲区的format必须要所播放音频的waveformat一致。

 Controls,不同的缓冲区的控制参数的值可以不一样,比如音量,频率,以及在不同方向的移动,当创建buffer时,你就要指定你需要的控制参数的值,例如,不要为一个不支持3D的音频创建一个3D缓冲区

 Location,你创建的缓冲区可以在硬件管理的内存中,也可以在软件管理的内存中,当然,硬件缓冲区比软件缓冲区速度要快。

 

 你可以调用IDirectSound8::CreateSoundBuffer函数来创建一个缓冲区(buffer)对象。这个函数返回一个指向IDrectSoundBuffer接口的指针,通过这个接口,应用程序可以获取

IDirectSoundBuffer8 interface.

 下面的一段代码演示了如何创建一个辅助缓冲区,并且返回一个IDirectSoundBuffer8接口

HRESULT CreateBasicBuffer(LPDIRECTSOUND8 lpDirectSound, LPDIRECTSOUNDBUFFER8* ppDsb8)

{

  WAVEFORMATEX wfx;

  DSBUFFERDESC dsbdesc;

  LPDIRECTSOUNDBUFFER pDsb = NULL;

  HRESULT hr;

 

  // Set up WAV format structure.

 

 

 

  memset(&wfx, 0, sizeof(WAVEFORMATEX));

  wfx.wFormatTag = WAVE_FORMAT_PCM;

  wfx.nChannels = 2;

  wfx.nSamplesPerSec = 22050;

  wfx.nBlockAlign = 4;

  wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;

  wfx.wBitsPerSample = 16;

 

  // Set up DSBUFFERDESC structure.

 

  memset(&dsbdesc, 0, sizeof(DSBUFFERDESC));

  dsbdesc.dwSize = sizeof(DSBUFFERDESC);

  dsbdesc.dwFlags =

    DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_CTRLFREQUENCY; //  buffer

  dsbdesc.dwBufferBytes = 3 * wfx.nAvgBytesPerSec;

  dsbdesc.lpwfxFormat = &wfx;

 

  // Create buffer.

 

  hr = lpDirectSound->CreateSoundBuffer(&dsbdesc, &pDsb, NULL);

  if (SUCCEEDED(hr))

  {

     hr = pDsb->QueryInterface(IID_IDirectSoundBuffer8, (LPVOID*) ppDsb8);

     pDsb->Release();

  }

  return hr;

}

  这个例子中创建了一个可以持续播放3秒钟的流缓冲区,如果你要创建静态的缓冲区,你就要创建的buffersize正好能够容下你的音频数据即可。

   如果缓冲区的位置没有指定,DirectSound在条件允许的情况下将你的缓冲区设置为硬件控制,因为硬件缓冲区的混音是通过声卡的加速器来进行的,受应用程序的影响较小。

  如果你想自己控制你创建的缓冲区(buffer)的位置,那么你一定要将DSBUFFERDESC中的 dsbdesc.dwFlags成员变量设置为DSBCAPS_LOCHARDWARE或者设置为DSBCAPS_LOCSOFTWARE,如果设置为DSBCAPS_LOCHARDWARE,此时硬件设备的资源不足时,缓冲区创建失败。

 如果你想使用DirectSound的管理声音的特性,那么你创建缓冲区的时候一定要设置DSBCAPS_LOCDEFER标志,这个标志表示只有在播放的时候才分配内存,更多的细节,你可以参考动态的声音管理一节。

   你可以通过IDirectSoundBuffer8::GetCaps方法来探明已经存在的缓冲区,并且可以检查该bufferdwFlags设置情况。

   缓冲区对象属于创建它的设备对象。当设备对象销毁时,它所创建的buffer对象也全部被销毁,没法被引用了。

   你可以通过IDirectSound8::DuplicateSoundBuffer同时创建两个或者多个包含相同数据的辅助缓冲区,当然不允许复制主缓冲区。因为复制的缓冲区和原始的缓冲区共享内存,改变复制的缓冲区的数据的同时,也改变了原始缓冲区的内容。

 

 

 

  下面我们看看buffer的控制属性

  当你创建了缓冲区的时候,你的应用程序设置该缓冲区的控制属性,这是由DSBUFFERDESC结构的dwFlags成员来控制的。

下面我们来看看dwFlags的取值

 DSBCAPS_CTRL3D  表示声音可以在3个方向上进行移动

 DSBCAPS_CTRLFX ,表示可以在缓冲区中添加特技

 DSBCAPS_CTRLFREQUENCY ,表示声音的频率可以被改动

 DSBCAPS_CTRLPAN,表示声音可以从左声道被移动到右声道

 DSBCAPS_CTRLPOSITIONNOTIFY,可以在buffer中设置通知的位置。

 DSBCAPS_CTRLVOLUME,声音的大小可以被改变。

注意,有些标志位的组合是不允许的,具体的信息可以参见DSBUFFERDESC结构。

 

 

 

 为了使得你的声卡更好的工作,你最好改变你应用程序用到的几个控制属性,其他的属性最好采用DirectSound来默认的设置。

  DirectSound通过控制属性来判断是否可以在硬件设备上分配缓冲区。例如,一个设备可能支持硬件缓冲区,但是不支持控制pan,此时,当DSBCAPS_CTRLVOLUME标志没有设置的时候,DirectSound就可以使用硬件加速。

 如果你的应用程序想调用buffer不支持的控制项,那么调用就会失败,例如,如果你想通过IDirectSoundBuffer8::SetVolume方法来设置音量,只有在创建buffer时设置了DSBCAPS_CTRLVOLUME标志这个函数调用才会成功。

 

 

 

   当你创建的辅助缓冲区的控制标志位被设置为DSBCAPS_CTRL3D时,如果该buffer创建的位置由软件控制,那么你就可以给你创建的缓冲区指定一个3D你的声音的算法,缺省的情况下,采用的是HRTFno head-related transfer function)算法来处理3D化声音。

 

 

 

 下面我们看看如何两种buffer如何播放声音的,先看看静态的缓冲区吧。

  包含全部音频数据的缓冲区我们称为静态的缓冲区,尽管,不同的声音可能会反复使用同一个内存buffer,但严格来说,静态缓冲区的数据只写入一次。

 静态缓冲区的创建和管理和流缓冲区很相似,唯一的区别就是它们使用的方式不一样,静态缓冲区只填充一次数据,然后就可以play,然而,流缓冲区是一边play,一边填充数据。

 

 

 

 给静态缓冲区加载数据分下面几个步骤

1,  调用IDirectSoundBuffer8::Lock函数来锁定所有的内存,你要指定你锁定内存中你开始写入数据的偏移位置,并且取回该偏移位置的地址。

2,  采用标准的数据copy方法,将音频数据复制到返回的地址。

3,  调用IDirectSoundBuffer8::Unlock.,解锁该地址。

 下面的例子演示了上面提到的几个步骤,lpdsbStatic是指向静态buffer的指针

LPVOID lpvWrite;

DWORD  dwLength;

 

 

 

if (DS_OK == lpdsbStatic->Lock(

      0,          // Offset at which to start lock.

      0,          // Size of lock; ignored because of flag.

      &lpvWrite,  // Gets address of first part of lock.

      &dwLength,  // Gets size of first part of lock.

      NULL,       // Address of wraparound not needed.

      NULL,       // Size of wraparound not needed.

      DSBLOCK_ENTIREBUFFER))  // Flag.

{

  memcpy(lpvWrite, pbData, dwLength);

  lpdsbStatic->Unlock(

      lpvWrite,   // Address of lock start.

      dwLength,   // Size of lock.

      NULL,       // No wraparound portion.

      0);         // No wraparound size.

}

else

(

  ErrorHandler();  // Add error-handling here.

}

将数据加载到缓冲区中就可以播放,调用IDirectSoundBuffer8::Play方法。如下:

lpdsbStatic->SetCurrentPosition(0);

HRESULT hr = lpdsbStatic->Play(

    0,  // Unused.

    0,  // Priority for voice management.

    0); // Flags.

if (FAILED(hr))

(

  ErrorHandler();  // Add error-handling here.

}

因为这个例子中没有设置DSBPLAY_LOOPING标志,当buffer到达最后时就会自动停止,你也可以调用IDirectSoundBuffer8::Stop方法来停止播放。如果你提前停止播放,播放光标的位置就会被保存下来,因此,例子中IDirectSoundBuffer8::SetCurrentPosition方法就是为了保证从头开始播放。

 

 

 

  下面我们看看流缓冲区的用法

   流缓冲区用来播放那些比较长的声音,因为数据比较长,没法一次填充到缓冲区中,一边播放,一边将新的数据填充到buffer中。

   可以通过IDirectSoundBuffer8::Play函授来播放缓冲区中的内容,注意在该函数的参数中一定要设置DSBPLAY_LOOPING标志。

   通过IDirectSoundBuffer8::Stop方法中断播放,该方法会立即停止缓冲区播放,因此你要确保所有的数据都被播放,你可以通过拖动播放位置或者设置通知位置来实现。

  将音频流倒入缓冲区需要下面三个步骤

1确保你的缓冲区已经做好接收新数据的准备。你可以拖放播放的光标位置或者等待通知

2调用IDirectSoundBuffer8::Lock.函数锁住缓冲区的位置,这个函数返回一个或者两个可以写入数据的地址

3使用标准的copy数据的方法将音频数据写入缓冲区中

IDirectSoundBuffer8::Unlock.,解锁

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

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

1内存地址的偏移位置20000

2从偏移位置到buffer的最末端的字节数,也是20000,你要在第一个地址写入20000个字节的内容

3偏移量为0的地址

4从起始点开始的字节数,也就是10000字节,你要将这个字节数的内容写入第二个地址。

如果不包含零点,最后两个数值为NULL0

当然,你也有可能锁定buffer的全部内存,建议你在播放的时候不要这么做,通过你只是更新所有buffer中的一部份,例如,你可能在播放广标到达1/2位置前要将第一个1/4内存更新成新的数据,你一定不要更新play光标和Write光标间的内容。

 

 

 

BOOL AppWriteDataToBuffer(

    LPDIRECTSOUNDBUFFER8 lpDsb,  // The buffer.

    DWORD dwOffset,              // Our own write cursor.

    LPBYTE lpbSoundData,         // Start of our data.

    DWORD dwSoundBytes)          // Size of block to copy.

{

  LPVOID  lpvPtr1;

  DWORD dwBytes1;

  LPVOID  lpvPtr2;

  DWORD dwBytes2;

  HRESULT hr;

 

  // Obtain memory address of write block. This will be in two parts

  // if the block wraps around.

 

  hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1,

      &dwBytes1, &lpvPtr2, &dwBytes2, 0);

 

  // If the buffer was lost, restore and retry lock.

 

  if (DSERR_BUFFERLOST == hr)

  {

    lpDsb->Restore();

    hr = lpDsb->Lock(dwOffset, dwSoundBytes,

        &lpvPtr1, &dwBytes1,

        &lpvPtr2, &dwBytes2, 0);

  }

  if (SUCCEEDED(hr))

  {

    // Write to pointers.

 

    CopyMemory(lpvPtr1, lpbSoundData, dwBytes1);

    if (NULL != lpvPtr2)

    {

      CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2);

    }

 

    // Release the data back to DirectSound.

 

    hr = lpDsb->Unlock(lpvPtr1, dwBytes1, lpvPtr2,

    dwBytes2);

    if (SUCCEEDED(hr))

    {

      // Success.

      return TRUE;

    }

  }

 

  // Lock, Unlock, or Restore failed.

 

  return FALSE;

}

 

 

 

下面我们看看如何控制播放的属性

你可以通过IDirectSoundBuffer8::GetVolume and IDirectSoundBuffer8::SetVolume函数来获取或者设置正在播放的音频的音量的大小。

 如果设置主缓冲区的音量就会改变声卡的音频的声量大小。音量的大小,用分贝来表示,一般没法来增强缺省的音量,这里要提示一下,分贝的增减不是线形的,减少3分贝相当于减少1/2的能量。最大值衰减100分贝几乎听不到了。

 通过IDirectSoundBuffer8::GetFrequency and IDirectSoundBuffer8::SetFrequency方法可以获取设置音频播放的频率,主缓冲区的频率不允许改动,

通过  IDirectSoundBuffer8::GetPan and IDirectSoundBuffer8::SetPan函数可以设置音频在左右声道播放的位置,具有3D特性的缓冲区没法调整声道。

 

 

 

  下面要看看混音。

   如果辅助缓冲区中的音频同时播放就会主缓冲区自动的混音,在WDM驱动模式下,混音的工作由核心混音器来完成,不同的辅助缓冲区可能具有不同的WAV格式(例如,不同的采样频率),在必要的时候,辅助缓冲区的格式要转换成主缓冲区,或者核心混音器的格式。

  VXD驱动模式下,如果你的辅助缓冲区都采用相同的音频格式,并且硬件的音频格式也和你的音频格式匹配,此时,混音器不用作任何的转换。你的应用程序可以创建一个主缓冲区,然后通过IDirectSoundBuffer8::SetFormat来设置硬件的输出格式。要注意,只有你的协作度一定要是Priority Cooperative Level.,并且,一定要创建辅助缓冲区前设置主缓冲区,DirectSound会将你的设置保存下来。

  WDM模式下,对主缓冲区的的设置没有作用,因为主缓冲区的格式是由内核混音器来决定的。

 

 

 

循环播放;

Directsound并不支持在buffer内部的循环或者声音的一部份循环,DSBPLAY_LOOPING标志是整个bufferend处然后重新从头的播放,如果你在静态的缓冲区的播放到末端后然后将play光标设置到起始位置重新播放,会导致audio glitches,因为DirectSound必须忽略所有的预处理的数据。所以,如果要循环播放,一定要在stream buffer中进行。

 

 

 

最后,我们来谈一下缓冲区管理。

  通过IDirectSoundBuffer8::GetCaps方法我们可以获取DirectSoundBuffer对象的一些属性。应用程序可以通过IDirectSoundBuffer8::GetStatus方法还获取buffer是播放还是停止状态。

   通过IDirectSoundBuffer8::GetFormat方法可以获取buffer中音频数据的格式,你也可以调用IDirectSoundBuffer8::SetFormat方法来主缓冲区的数据格式。

  Sound Buffer中的一些数据在某些条件下有可能丢失,例如,如果buffer位于声卡的内存中,此时其他的应用程序获取硬件的控制权并请求资源。当具有Write_primary权限的应用程序moves to forground,此时,Directsound就会使其他的buffer内容丢失才能够让foreground的应用程序直接向主缓冲区中写数据。

   IDirectSoundBuffer8::Lock or IDirectSoundBuffer8::Play方法向一个lost buffer进行操作时,就会返回一个错误码。当造成buffer丢失的应用程序降低协作度,低于write_primary,或者moves to background,其他的应用程序可以调用IDirectSoundBuffer8::Restore来重新分配内存,如果成功,这个方法就会恢复内存中的内容以及对该内存的设置,但是,恢复的缓冲区中不包含合法的数据,因此,应用程序要重新向该buffer中填写数据。