游戏音乐与音效的播放

游戏音乐与音效的播放
游戏音乐与音效的播放

游戏音乐与音效的播放

发表日期:2006-08-23 作者:[转贴] 出处:目录:

文档内容:

在Win32环境下,播放音乐音效的方法太多了,而且有一个共同点就

是:你不需要花很大的心力就可以得到你需要的东西。延续主题式的

探讨,这一期我们着重在音乐与音效的播放。

□游戏的配乐

我相信很多人一定同意音乐在游戏里面所占的地位,回想一下国内RPG的

经典「仙剑奇侠传」,剥掉音乐这一个层面,整个游戏将会逊色不少,尤其

适当的场景搭配适当的音乐,更能让玩家融入剧情当中。该哭的时候哭,该

笑的时候笑,大概就很切中要领了。RPG剩下的音效部份,并不特别突出,

大抵上知道砍人的时候有挥剑的声音就可以了,所以在音效的表现方面,通

常比较不那麽注重。而即时战斗的游戏着重在厮杀的音效表现上,一大片人

马,一片混杂的声音,这其中牵涉到混音的部份,我们底下也会探讨到。读

完这篇文章,你会学习到什麽时候该用什麽样的程式作法来表现游戏的另一

个生命:音乐与音效。

□从MIDI开始

早期DOS下的音乐部份,大多数采用声霸卡的规格,副档名为CMF者便是

这种格式,当然游戏通常不会让你看到真正的作法,但是内部采用这种格式

居多是无庸置疑的。而WINDOW下的游戏以光碟发行者居多,为了充分达

到空间利用的阶段,游戏中会大量使用WAV格式的档案,或是直接将音乐

烧成音轨的格式。尤其很多游戏喜欢采用第一片资料片,第二片音乐片的作

法,平常不玩游戏还可以当成音乐CD来听,算是满有质感的一件事。当然,

我的意思是这些音乐必须要声声入耳,如果音乐本身庸庸碌碌的,即使烧成

音轨,一样是庸庸碌碌,改变不了这个事实。

在WINDOW下,考量到空间的大小,MIDI格式的音乐档绝对是最佳的选择,一首五分钟的MIDI了不起十万字元的大小,这跟WAV格式一分钟占用量以MB计,简直是小巫见大巫,所以网站上的音乐,游戏的音乐,都很适合用MIDI来表现,而音乐部份我个人注重旋律,至於一首音乐本身使用到的乐器数量,我倒是很少去注意,人的耳朵听东西有一定的极限,只要不产生杂音,配合优美的旋律,大致上都可以接受。

□播放MIDI的程式作法

游戏中播放音乐的要点就是循环播放,也就是播放完毕以後,要让他从头开始播放,直到场景更换,或是游戏结束为止。所以当MIDI档案播放完毕以後,必须要能通知程式,让程式做出适当的处理。播放MIDI的作法只要藉由WINDOW的多媒体的支援,马上就搞定了,甚至直接从HELP的作法剪过来,稍微修改一下,也能符合需要,因为这种东西相当公式化,A君和B 君写出来的程式码也大致上会长得差不多,废话不多说,看看程式多麽简单便是:

class CMidi

{

public:

DWORD Play(HWND,char* FileName);

void Replay();

void Stop();

private:

UINT wDeviceID; wDeviceID = ;

MIDI = MCI_SEQ_STATUS_PORT;

if (dwReturn = mciSendCommand(wDeviceID, MCI_STATUS,

MCI_STATUS_ITEM, (DWORD)(LPVOID)

&mciStatusParms))

{

mciSendCommand(wDeviceID, MCI_CLOSE, 0, NULL);

return (dwReturn);

}

0L

= sizeof(DSBUFFERDESC);

//使用默认的设置(音量之类)

= DSBCAPS_CTRLDEFAULT;

//3秒钟长度的缓冲(3-second buffer)

= 3 *

= (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));

= WAVE_FORMAT_PCM;

= 2;

= 22050;

= 4;

=

* ;

= 16;

//初始化DSBUFFERDESC结构

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

= DSBCAPS_PRIMARYBUFFER;

//缓冲的大小是由硬件决定的

= 0;

= 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)

{

//获得主缓冲的大小

= sizeof(DSBCAPS);

(*lplpDsb)->lpVtbl->GetCaps(*lplpDsb, &dsbcaps);

*lpdwBufferSize = ;

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;

= DSBPN_OFFSETSTOP;

= 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;

}

相关主题
相关文档
最新文档