今天要讲的是在dx中演奏音乐和音效,没看过这篇文章前,相信很多人认为音乐和音效的区别主要是播发时间长短的问题,实际一看,这里面十分的玄妙。
有一点先说一下,相比于图形技术的更新换代,音乐技术没有发生过多大的变化,dx3开始一直到dx7的时候,都是使用dx3的接口。直到dx8才把Sound和Music整合成了Audio。
那么音乐(Music)和音效(Sound)究竟有什么区别呢?
所谓Sound,专业点来说就是数字化的数码声音。比如你买了个麦克风,你干吼几下,然后录音软件把你的声音记录成了一个文件,那个文件就是音效了(很渗人啊)。
而Music是合成音,直接取缔了录音的阶段,用户直接编入数据,让音频发生库发出数据所代表的音乐。
文件格式上来说,Sound代表格式是WAV,而Music是MIDI。
了解了这一些就知道为什么要分成DSound和DMusic了。好了下面是一个实际的程序,先我们使用老接口的Sound来播放一个音效。
申明一下,这里工程中的基础代码是上一节使用过代码,有需要的玩家可以看一下dxinput的内容。
首先需要引入dsound.lib和winmm.lib,但后者是windows部分用于读取媒体文件,所以只需要使用vs自带的lib就OK了。
然后引入头文件#include <mmsystem.h> #include "dsound.h"这两个。
环境搞定之后,下面是编写一些全局代码,做一些准备工作:
- LPDIRECTSOUND lpds;
- DSBUFFERDESC dsbd;
-
-
- typedef struct pcm_sound_typ
- {
- LPDIRECTSOUNDBUFFER dsbuffer;
- int state;
- int rate;
- int size;
- int id;
- }pcm_sound, *pcm_sound_ptr;
-
- #define SOUND_NULL 0
- #define SOUND_LOADED 1
-
- const int MAX_SOUNDS = 10;
- pcm_sound sound_fx[MAX_SOUNDS];
然后我们要实现的效果就是在游戏初始化的时候读取一个文件,并播放,我看了一下就选wind.wav这个风的音效(不知道从哪录的,怪渗人的)。
下面是初始化并播放的代码:
-
- if (DirectSoundCreate(NULL, &lpds, NULL)!=DS_OK)
- {
- popMessage(TEXT("创建音效对象失败"));
- return 0;
- }
-
- if (FAILED(lpds->SetCooperativeLevel(main_window_handle, DSSCL_NORMAL)))
- {
- popMessage(TEXT("ds设置协作对象失败"));
- return 0;
- }
-
-
- memset(sound_fx, 0, sizeof(pcm_sound)*MAX_SOUNDS);
- for (int i = 0; i < MAX_SOUNDS; i)
- {
- if (sound_fx[i].dsbuffer)
- {
- sound_fx[i].dsbuffer->Stop();
- sound_fx[i].dsbuffer->Release();
- }
- memset(&sound_fx[i], 0, sizeof(pcm_sound));
- sound_fx[i].state = SOUND_NULL;
- sound_fx[i].id = i;
- }
-
- int id = DSound_Load_WAV(TEXT("wind.wav"));
- if (id == -1)
- return 0;
- sound_fx[id].dsbuffer->Play(0, 0, DSBPLAY_LOOPING);
注意不要忘了游戏结束时的释放:
- for (DWORD i = 0; i < MAX_SOUNDS; i)
- {
- if(sound_fx[i].dsbuffer)
- sound_fx[i].dsbuffer->Release();
- }
- if(lpds)
- lpds->Release();
看到这里,玩家们是不是感觉很简单?哈哈,其实还有个大头没有做呢,就是要将磁盘上的数据读取到sound_fx数组中,是我们之前没有提到的DSound_Load_WAV方法。
下面我们来看一下:
-
- int DSound_Load_WAV(LPWSTR filename, bool is_default = true)
- {
-
-
- HMMIO hwav;
- MMCKINFO parent;
- MMCKINFO child;
- WAVEFORMATEX wfmtx;
- int sound_id = -1;
- int index;
-
- UCHAR* snd_buffer;
- UCHAR* audio_ptr_1 = NULL;
- UCHAR* audio_ptr_2 = NULL;
- DWORD audio_length_1 = 0;
- DWORD audio_length_2 = 0;
-
-
- for (index = 0; index < MAX_SOUNDS; index)
- {
- if (sound_fx[index].state == SOUND_NULL)
- {
- sound_id = index;
- break;
- }
- }
-
- if (sound_id == -1)
- {
- popMessage(TEXT("音乐存放空间不足"));
- return -1;
- }
-
-
- parent.ckid = (FOURCC)0;
- parent.cksize = 0;
- parent.fccType = (FOURCC)0;
- parent.dwDataOffset = 0;
- parent.dwFlags = 0;
- child = parent;
-
-
- if ((hwav = mmioOpen(filename, NULL, MMIO_READ | MMIO_ALLOCBUF)) == NULL)
- {
- popMessage(TEXT("打开WAV文件失败"));
- return -1;
- }
-
-
-
- parent.fccType = mmioFOURCC('W', 'A', 'V', 'E');
- if (mmioDescend(hwav, &parent, NULL, MMIO_FINDRIFF))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 指针移动失败了"));
- return -1;
- }
-
-
- child.ckid = mmioFOURCC('f', 'm', 't', ' ');
- if (mmioDescend(hwav, &child, &parent, 0))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV FMT 指针移动失败了"));
- return -1;
- }
-
-
- if (mmioRead(hwav, (char*)&wfmtx, sizeof(wfmtx)) != sizeof(wfmtx))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 信息格式读取失败了"));
- return -1;
- }
-
- if (wfmtx.wFormatTag != WAVE_FORMAT_PCM)
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 信息格式不是PCM的失败"));
- return -1;
- }
-
-
- if (mmioAscend(hwav, &child, 0))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 指针移动到结尾失败"));
- return -1;
- }
-
-
- child.ckid = mmioFOURCC('d', 'a', 't', 'a');
- if (mmioDescend(hwav, &child, &parent, MMIO_FINDCHUNK))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 指针移动到数据块失败"));
- return -1;
- }
-
-
- snd_buffer = (UCHAR*)malloc(child.cksize);
- mmioRead(hwav, (char*)snd_buffer, child.cksize);
- mmioClose(hwav, 0);
-
-
- sound_fx[sound_id].rate = wfmtx.nSamplesPerSec;
- sound_fx[sound_id].size = child.cksize;
- sound_fx[sound_id].state = SOUND_LOADED;
-
-
-
- WAVEFORMATEX pcmwf;
- memset(&pcmwf, 0, sizeof(WAVEFORMATEX));
- pcmwf.wFormatTag = WAVE_FORMAT_PCM;
- pcmwf.nChannels = 1;
- pcmwf.nSamplesPerSec = 11025;
- pcmwf.nBlockAlign = 1;
- pcmwf.nAvgBytesPerSec = pcmwf.nSamplesPerSec * pcmwf.nBlockAlign;
- pcmwf.wBitsPerSample = 8;
- pcmwf.cbSize = 0;
-
-
-
- dsbd.dwSize = sizeof(DSBUFFERDESC);
- if (is_default)
- dsbd.dwFlags = DSBCAPS_CTRLFREQUENCY | DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_STATIC | DSBCAPS_LOCSOFTWARE;
- dsbd.dwBufferBytes = child.cksize;
- dsbd.lpwfxFormat = &pcmwf;
-
-
- if (lpds->CreateSoundBuffer(&dsbd, &sound_fx[sound_id].dsbuffer, NULL) != DS_OK)
- {
- free(snd_buffer);
- popMessage(TEXT("创建声音缓存失败"));
- return -1;
- }
-
- if (sound_fx[sound_id].dsbuffer->Lock(0, child.cksize, (void**)&audio_ptr_1, &audio_length_1, (void**)&audio_ptr_2, &audio_length_2, DSBLOCK_FROMWRITECURSOR) != DS_OK)
- {
- popMessage(TEXT("ds加锁失败了"));
- return 0;
- }
-
- memcpy(audio_ptr_1, snd_buffer, audio_length_1);
- memcpy(audio_ptr_2, snd_buffer audio_length_1, audio_length_2);
-
- if (sound_fx[sound_id].dsbuffer->Unlock(audio_ptr_1, audio_length_1, audio_ptr_2, audio_length_2) != DS_OK)
- {
- popMessage(TEXT("ds解锁失败了"));
- return 0;
- }
-
- free(snd_buffer);
-
- return sound_id;
-
- }
是不是感觉要疯了?心中就在想,微软你能不能做点好事,把这些读取具体文件的方法封装一下啊。
嘛,反正这边就是使用mmio来读取,音效对象lpds加锁解锁,将数据读入。仔细看看其实也没什么东西,就是指针的移动可能有些难以理解。
运行一下就能听到声音了。
好了,接下去的内容是Music。可能是Sound比较老了需要自己编写加载程序,但我们的Music可就牛多了,它读取方法已经封装好,而且它是dx中第一个完全COM化的组件,意味着不需要一如lib,只要引入头文件就好。
嘛,就是头文件有点多:
- #include "dmksctrl.h"
- #include "dmplugin.h"
- #include "dmusicc.h"
- #include "dmusicf.h"
- #include "dmusici.h"
同样的先做准备工作:
- #define MULTI_TO_WIDE( x,y ) MultiByteToWideChar( CP_ACP,MB_PRECOMPOSED, y,-1,x,_MAX_PATH); //字节到宽字符串的转换,没错我们重要牛B了,要使用到宽字符串了
-
- IDirectMusicPerformance *dm_perf = NULL;
- IDirectMusicLoader *dm_loader = NULL;
-
-
- typedef struct DMUSIC_MIDI_TYP
- {
- IDirectMusicSegment *dm_segment;
- IDirectMusicSegmentState *dm_segstate;
- int id;
- int state;
- }DMUSIC_MIDI, *DMUSIC_MIDI_PTR;
-
- #define MIDI_NULL 0
- #define MIDI_LOADED 1
-
- #define DM_NUM_SEGMENTS 64 //音乐的最大数量
- DMUSIC_MIDI dm_midi[DM_NUM_SEGMENTS];
-
- int now_music_id = -1;
- int count = 0;
接下来我们要实现一个什么样的效果呢?我打算一开始播放音乐,隔十秒后停止,然后再隔十秒再播放,再隔十秒再停止,如此反复。嘛,这就是我要定义计数的原因了。
首先看看初始化:
-
- if (FAILED(CoInitialize(NULL)))
- {
- popMessage(TEXT("COM初始化失败"));
- return 0;
- }
-
-
- if (FAILED(CoCreateInstance(CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance, (void**)&dm_perf)))
- {
- popMessage(TEXT("音乐表演对象创建失败"));
- return 0;
- }
-
- if (FAILED(dm_perf->Init(NULL, lpds, main_window_handle)))
- {
- popMessage(TEXT("音乐表演对象初始化失败"));
- return 0;
- }
-
- if (FAILED(dm_perf->AddPort(NULL)))
- {
- popMessage(TEXT("音乐表演对象创建端口失败"));
- return 0;
- }
-
- if (FAILED(CoCreateInstance(CLSID_DirectMusicLoader, NULL, CLSCTX_INPROC, IID_IDirectMusicLoader, (void**)&dm_loader)))
- {
- popMessage(TEXT("创建音乐加载对象失败"));
- return 0;
- }
-
-
- now_music_id = DMusic_Load_MIDI(TEXT("midifile8.mid"));
- if (now_music_id == -1)
- return 0;
这边我选取了一首非常激昂的音乐,听说恐怖的环境音跟激昂的战斗音乐更搭哦。
这边看到了DMusic_Load_MIDI方法,因为dx对读取做了一层封装,所以这个函数的实现没有那么复杂。我们有理由相信,随着dx版本的迭代,使用这类方法会越来越方便。
让我们来看看load方法:
-
- int DMusic_Load_MIDI(LPWSTR filename)
- {
- DMUS_OBJECTDESC objDesc;
- HRESULT hr;
- IDirectMusicSegment *pSegment = NULL;
-
- int id = -1;
- for (int index = 0; index < DM_NUM_SEGMENTS; index)
- {
- if (dm_midi[index].state == MIDI_NULL)
- {
- id = index;
- break;
- }
- }
-
- if (id == -1)
- {
- popMessage(TEXT("音乐已经放满了"));
- return -1;
- }
-
-
- char szDir[_MAX_PATH];
- WCHAR wszDir[_MAX_PATH];
- if (_getcwd(szDir, _MAX_PATH) == NULL)
- {
- popMessage(TEXT("获取工作目录失败"));
- return -1;
- }
- MULTI_TO_WIDE(wszDir, szDir);
-
-
- hr = dm_loader->SetSearchDirectory(GUID_DirectMusicAllTypes, wszDir, FALSE);
- if (FALSE(hr))
- {
- popMessage(TEXT("设置查找目录失败"));
- return -1;
- }
-
-
- DDRAW_INIT_STRUCT(objDesc);
- objDesc.guidClass = CLSID_DirectMusicSegment;
- wcscpy_s(objDesc.wszFileName, filename);
- objDesc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_FILENAME;
-
-
- dm_loader->GetObjectW(&objDesc, IID_IDirectMusicSegment, (void**)&pSegment);
- if (FAILED(hr))
- {
- popMessage(TEXT("获取音乐失败"));
- return -1;
- }
-
-
- pSegment->SetParam(GUID_StandardMIDIFile, -1, 0, 0, (void*)dm_perf);
- pSegment->SetParam(GUID_Download, -1, 0, 0, (void*)dm_perf);
-
- dm_midi[id].dm_segment = pSegment;
- dm_midi[id].dm_segstate = NULL;
- dm_midi[id].state = MIDI_LOADED;
-
- return id;
- }
不要忘了游戏结束的释放:
- dm_perf->Stop(NULL, NULL, 0, 0);
- for (DWORD i = 0; i < DM_NUM_SEGMENTS; i)
- {
- if(dm_midi[i].dm_segment)
- {
- dm_midi[i].dm_segment->SetParam(GUID_Unload, -1, 0, 0, (void*)dm_perf);
- dm_midi[i].dm_segment->Release();
- }
- }
- dm_perf->CloseDown();
- dm_perf->Release();
-
- dm_loader->Release();
-
- CoUninitialize();
最后在主循环中添加一下代码:
- if (count % 600 == 0)
- {
- dm_perf->PlaySegment(dm_midi[now_music_id].dm_segment, 0, 0, &dm_midi[now_music_id].dm_segstate);
- }
- if (count % 600 == 300)
- {
- dm_perf->Stop(dm_midi[now_music_id].dm_segment, NULL, 0, 0);
- }
- count;
每个十秒播放停止就完成了。
这样Windows2d的部分就完结了,说不定会试一下写个什么算法的玩玩。不过等继续这个课程应该要到3d部分了。