3.2Dsound的buffer对象(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方法来探明已经存在的缓冲区,并且可以检查该buffer的dwFlags设置情况。
缓冲区对象属于创建它的设备对象。当设备对象销毁时,它所创建的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你的声音的算法,缺省的情况下,采用的是HRTF(no 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数据的方法将音频数据写入缓冲区中
4 IDirectSoundBuffer8::Unlock.,解锁
IDirectSoundBuffer8::Lock可能返回两个地址的原因在于你锁定内存的数量是随机的,有时你锁定的区域正好包含buffer的起始点,这时,就会给你返回两个地址,举个例子吧
假设你锁定了30,000字节,偏移位置为20,000字节,也就是开始位置,如果你的缓冲区的大小为40,000字节,此时就会给你返回四个数据
1内存地址的偏移位置20,000,
2从偏移位置到buffer的最末端的字节数,也是20,000,你要在第一个地址写入20,000个字节的内容
3偏移量为0的地址
4从起始点开始的字节数,也就是10,000字节,你要将这个字节数的内容写入第二个地址。
如果不包含零点,最后两个数值为NULL和0,
当然,你也有可能锁定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标志是整个buffer到end处然后重新从头的播放,如果你在静态的缓冲区的播放到末端后然后将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中填写数据。