Windows编程 DirectSound DirectMusic 音效和音乐

发表于2017-11-08
评论0 771浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

今天要讲的是在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"这两个。

 

环境搞定之后,下面是编写一些全局代码,做一些准备工作:

  1. LPDIRECTSOUND lpds; //dx音效对象  
  2. DSBUFFERDESC dsbd;  //音效缓存描述  
  3.   
  4. // WAV文件读入后存放的类  
  5. typedef struct pcm_sound_typ  
  6. {  
  7.     LPDIRECTSOUNDBUFFER dsbuffer;  
  8.     int state;  
  9.     int rate;  
  10.     int size;  
  11.     int id;  
  12. }pcm_sound, *pcm_sound_ptr;  
  13.   
  14. #define SOUND_NULL 0  
  15. #define SOUND_LOADED 1  
  16.   
  17. const int MAX_SOUNDS = 10;  //最多存放音效的数量  
  18. pcm_sound sound_fx[MAX_SOUNDS];     //所有的音效  

然后我们要实现的效果就是在游戏初始化的时候读取一个文件,并播放,我看了一下就选wind.wav这个风的音效(不知道从哪录的,怪渗人的)。

 

下面是初始化并播放的代码:

  1. // 开始创建音效对象  
  2. if (DirectSoundCreate(NULL, &lpds, NULL)!=DS_OK)  
  3. {  
  4.     popMessage(TEXT("创建音效对象失败"));  
  5.     return 0;  
  6. }  
  7.   
  8. if (FAILED(lpds->SetCooperativeLevel(main_window_handle, DSSCL_NORMAL))) //设置普通的协作等级  
  9. {  
  10.     popMessage(TEXT("ds设置协作对象失败"));  
  11.     return 0;  
  12. }  
  13.   
  14. // 初始化所有的音效  
  15. memset(sound_fx, 0, sizeof(pcm_sound)*MAX_SOUNDS);  
  16. for (int i = 0; i < MAX_SOUNDS;  i)  
  17. {  
  18.     if (sound_fx[i].dsbuffer)  
  19.     {  
  20.         sound_fx[i].dsbuffer->Stop();  
  21.         sound_fx[i].dsbuffer->Release();  
  22.     }  
  23.     memset(&sound_fx[i], 0, sizeof(pcm_sound));  
  24.     sound_fx[i].state = SOUND_NULL;  
  25.     sound_fx[i].id = i;  
  26. }  
  27.   
  28. int id = DSound_Load_WAV(TEXT("wind.wav")); //获取音效  
  29. if (id == -1)  
  30.     return 0;  
  31. sound_fx[id].dsbuffer->Play(0, 0, DSBPLAY_LOOPING);  //播放!  

注意不要忘了游戏结束时的释放:

  1. for (DWORD i = 0; i < MAX_SOUNDS;  i)   //释放音效相关的对象  
  2. {  
  3.     if(sound_fx[i].dsbuffer)  
  4.         sound_fx[i].dsbuffer->Release();  
  5. }  
  6. if(lpds)  
  7.     lpds->Release();  

看到这里,玩家们是不是感觉很简单?哈哈,其实还有个大头没有做呢,就是要将磁盘上的数据读取到sound_fx数组中,是我们之前没有提到的DSound_Load_WAV方法。

 

下面我们来看一下:

  1. // 读取WAV文件  
  2. int DSound_Load_WAV(LPWSTR filename, bool is_default = true)  
  3. {  
  4.     // 使用windows的mmio来读取文件(使用这些类请先包含mmsystem.h头文件)  
  5.     // 初始化  
  6.     HMMIO hwav; //文件句柄  
  7.     MMCKINFO parent;    //父类块  
  8.     MMCKINFO child; //子类块  
  9.     WAVEFORMATEX wfmtx; //文件信息结构  
  10.     int sound_id = -1;  //音效的3唯一标识  
  11.     int index;  
  12.   
  13.     UCHAR* snd_buffer;  //文件数据缓存  
  14.     UCHAR* audio_ptr_1 = NULL;  //文件指针1  
  15.     UCHAR* audio_ptr_2 = NULL;  //文件指针2  
  16.     DWORD audio_length_1 = 0;   //两个缓存的长度  
  17.     DWORD audio_length_2 = 0;  
  18.   
  19.     // 寻找可用空间  
  20.     for (index = 0; index < MAX_SOUNDS;  index)  
  21.     {  
  22.         if (sound_fx[index].state == SOUND_NULL)    //找到可存放的空间  
  23.         {  
  24.             sound_id = index;  
  25.             break;  
  26.         }  
  27.     }  
  28.   
  29.     if (sound_id == -1) //没有找到可存放空间直接跳出  
  30.     {  
  31.         popMessage(TEXT("音乐存放空间不足"));  
  32.         return -1;  
  33.     }  
  34.   
  35.     // 初始化信息块  
  36.     parent.ckid = (FOURCC)0;  
  37.     parent.cksize = 0;  
  38.     parent.fccType = (FOURCC)0;  
  39.     parent.dwDataOffset = 0;  
  40.     parent.dwFlags = 0;  
  41.     child = parent;  
  42.   
  43.     // 打开文件到hwav中  
  44.     if ((hwav = mmioOpen(filename, NULL, MMIO_READ | MMIO_ALLOCBUF)) == NULL)  
  45.     {  
  46.         popMessage(TEXT("打开WAV文件失败"));  
  47.         return -1;  
  48.     }  
  49.   
  50.     // 注意:一个WAV文件由3部分组成:声纹riff标准格式、wavfmt文件标识、data数据部分  
  51.     // 使用定义的文件格式,将指针指到riff  
  52.     parent.fccType = mmioFOURCC('W''A''V''E');    //设置文件的媒体格式,当然是WAV啦  
  53.     if (mmioDescend(hwav, &parent, NULL, MMIO_FINDRIFF))  
  54.     {  
  55.         mmioClose(hwav, 0);  
  56.         popMessage(TEXT("WAV 指针移动失败了"));  
  57.         return -1;  
  58.     }  
  59.   
  60.     // 指针移动到fmt  
  61.     child.ckid = mmioFOURCC('f''m''t'' ');  
  62.     if (mmioDescend(hwav, &child, &parent, 0))  
  63.     {  
  64.         mmioClose(hwav, 0);  
  65.         popMessage(TEXT("WAV FMT 指针移动失败了"));  
  66.         return -1;  
  67.     }  
  68.   
  69.     // 读取wav格式信息  
  70.     if (mmioRead(hwav, (char*)&wfmtx, sizeof(wfmtx)) != sizeof(wfmtx))  
  71.     {  
  72.         mmioClose(hwav, 0);  
  73.         popMessage(TEXT("WAV 信息格式读取失败了"));  
  74.         return -1;  
  75.     }  
  76.   
  77.     if (wfmtx.wFormatTag != WAVE_FORMAT_PCM)    //确定格式是PCM的  
  78.     {  
  79.         mmioClose(hwav, 0);  
  80.         popMessage(TEXT("WAV 信息格式不是PCM的失败"));  
  81.         return -1;  
  82.     }  
  83.   
  84.     // 指针移动到信息块的结尾  
  85.     if (mmioAscend(hwav, &child, 0))  
  86.     {  
  87.         mmioClose(hwav, 0);  
  88.         popMessage(TEXT("WAV 指针移动到结尾失败"));  
  89.         return -1;  
  90.     }  
  91.   
  92.     // 指针移动的数据块  
  93.     child.ckid = mmioFOURCC('d''a''t''a');  
  94.     if (mmioDescend(hwav, &child, &parent, MMIO_FINDCHUNK))  
  95.     {  
  96.         mmioClose(hwav, 0);  
  97.         popMessage(TEXT("WAV 指针移动到数据块失败"));  
  98.         return -1;  
  99.     }  
  100.   
  101.     // 取数据  
  102.     snd_buffer = (UCHAR*)malloc(child.cksize);  
  103.     mmioRead(hwav, (char*)snd_buffer, child.cksize);  
  104.     mmioClose(hwav, 0);  
  105.   
  106.     // 设置相关的参数  
  107.     sound_fx[sound_id].rate = wfmtx.nSamplesPerSec;  
  108.     sound_fx[sound_id].size = child.cksize;  
  109.     sound_fx[sound_id].state = SOUND_LOADED;  
  110.   
  111.     // 下面这里是dxSound的部分,创建缓存(相当于ddraw中的表面),供声音对象使用  
  112.     // 声音信息结构  
  113.     WAVEFORMATEX pcmwf;  
  114.     memset(&pcmwf, 0, sizeof(WAVEFORMATEX));  
  115.     pcmwf.wFormatTag = WAVE_FORMAT_PCM; //WAV格式  
  116.     pcmwf.nChannels = 1;    //单声道  
  117.     pcmwf.nSamplesPerSec = 11025;   //采样频率频率  
  118.     pcmwf.nBlockAlign = 1;  //采样位数(这里表明一个字节)  
  119.     pcmwf.nAvgBytesPerSec = pcmwf.nSamplesPerSec * pcmwf.nBlockAlign;   //每秒采样的字节数  
  120.     pcmwf.wBitsPerSample = 8;   //采样位数(这里表明8bit)  
  121.     pcmwf.cbSize = 0;  
  122.   
  123.     // 创建声音缓存描述  
  124.   
  125.     dsbd.dwSize = sizeof(DSBUFFERDESC);  
  126.     if (is_default)  
  127.         dsbd.dwFlags = DSBCAPS_CTRLFREQUENCY | DSBCAPS_CTRLPAN | DSBCAPS_CTRLVOLUME | DSBCAPS_STATIC | DSBCAPS_LOCSOFTWARE; //设置标志,从左往右分别是频率控制、平衡控制、音量控制、用于静态数据、使用软件混音。  
  128.     dsbd.dwBufferBytes = child.cksize;  
  129.     dsbd.lpwfxFormat = &pcmwf;  
  130.   
  131.     // 创建声音缓存  
  132.     if (lpds->CreateSoundBuffer(&dsbd, &sound_fx[sound_id].dsbuffer, NULL) != DS_OK)  
  133.     {  
  134.         free(snd_buffer);  
  135.         popMessage(TEXT("创建声音缓存失败"));  
  136.         return -1;  
  137.     }  
  138.   
  139.     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)  //加锁  
  140.     {  
  141.         popMessage(TEXT("ds加锁失败了"));  
  142.         return 0;  
  143.     }  
  144.   
  145.     memcpy(audio_ptr_1, snd_buffer, audio_length_1);    //将两块对应的数据拷贝到指针中(算是sound_fx的dsbuffer中)  
  146.     memcpy(audio_ptr_2, snd_buffer   audio_length_1, audio_length_2);  
  147.   
  148.     if (sound_fx[sound_id].dsbuffer->Unlock(audio_ptr_1, audio_length_1, audio_ptr_2, audio_length_2) != DS_OK)  //解锁  
  149.     {  
  150.         popMessage(TEXT("ds解锁失败了"));  
  151.         return 0;  
  152.     }  
  153.   
  154.     free(snd_buffer);  
  155.   
  156.     return sound_id;  
  157.   
  158. }  

是不是感觉要疯了?心中就在想,微软你能不能做点好事,把这些读取具体文件的方法封装一下啊。

 

嘛,反正这边就是使用mmio来读取,音效对象lpds加锁解锁,将数据读入。仔细看看其实也没什么东西,就是指针的移动可能有些难以理解。


运行一下就能听到声音了。

 

好了,接下去的内容是Music。可能是Sound比较老了需要自己编写加载程序,但我们的Music可就牛多了,它读取方法已经封装好,而且它是dx中第一个完全COM化的组件,意味着不需要一如lib,只要引入头文件就好。

 

嘛,就是头文件有点多:

  1. #include "dmksctrl.h"  
  2. #include "dmplugin.h"  
  3. #include "dmusicc.h"  
  4. #include "dmusicf.h"  
  5. #include "dmusici.h"  

同样的先做准备工作:

  1. #define MULTI_TO_WIDE( x,y )  MultiByteToWideChar( CP_ACP,MB_PRECOMPOSED, y,-1,x,_MAX_PATH);    //字节到宽字符串的转换,没错我们重要牛B了,要使用到宽字符串了  
  2.   
  3. IDirectMusicPerformance *dm_perf = NULL;    //dx音乐表演对象,是不是感觉有点奇怪,不用惊讶,音乐对象(IDirectMusic)也是有的,会在表演对象创建的时候在后台创建,你不会接触到他。  
  4. IDirectMusicLoader *dm_loader = NULL;   //音乐加载器  
  5.   
  6. // MIDI文件读取后存放的类  
  7. typedef struct DMUSIC_MIDI_TYP  
  8. {  
  9.     IDirectMusicSegment *dm_segment;  
  10.     IDirectMusicSegmentState *dm_segstate;  
  11.     int id;  
  12.     int state;  
  13. }DMUSIC_MIDI, *DMUSIC_MIDI_PTR;  
  14.   
  15. #define MIDI_NULL 0  
  16. #define MIDI_LOADED 1  
  17.   
  18. #define DM_NUM_SEGMENTS 64  //音乐的最大数量  
  19. DMUSIC_MIDI dm_midi[DM_NUM_SEGMENTS];   //所有的音乐  
  20.   
  21. int now_music_id = -1;  //当前播放音乐的id  
  22. int count = 0;  //计数  

接下来我们要实现一个什么样的效果呢?我打算一开始播放音乐,隔十秒后停止,然后再隔十秒再播放,再隔十秒再停止,如此反复。嘛,这就是我要定义计数的原因了。

 

首先看看初始化:

  1. // 初始化COM  
  2. if (FAILED(CoInitialize(NULL)))  
  3. {  
  4.     popMessage(TEXT("COM初始化失败"));  
  5.     return 0;  
  6. }  
  7.   
  8. // 初始化音乐  
  9. if (FAILED(CoCreateInstance(CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance, (void**)&dm_perf))) //稍微有点诡异的创建方法,而且为什么要这么多id。。。  
  10. {  
  11.     popMessage(TEXT("音乐表演对象创建失败"));  
  12.     return 0;  
  13. }  
  14.   
  15. if (FAILED(dm_perf->Init(NULL, lpds, main_window_handle)))   //表演对象初始化,注意传入音效对象,如果没有音效对象的话直接传NULL,但是没有音效的游戏额,就像美食没有嚼劲,干瘪瘪的摊在舌头牙齿上。  
  16. {  
  17.     popMessage(TEXT("音乐表演对象初始化失败"));  
  18.     return 0;  
  19. }  
  20.       
  21. if (FAILED(dm_perf->AddPort(NULL)))  //创建一个端口,用于音乐合成  
  22. {  
  23.     popMessage(TEXT("音乐表演对象创建端口失败"));  
  24.     return 0;  
  25. }  
  26.   
  27. if (FAILED(CoCreateInstance(CLSID_DirectMusicLoader, NULL, CLSCTX_INPROC, IID_IDirectMusicLoader, (void**)&dm_loader))) //创建音乐加载对象,跟音乐表演对象一个思路  
  28. {  
  29.     popMessage(TEXT("创建音乐加载对象失败"));  
  30.     return 0;  
  31. }  
  32.   
  33. // 读取音乐文件  
  34. now_music_id = DMusic_Load_MIDI(TEXT("midifile8.mid"));  
  35. if (now_music_id == -1)  
  36.     return 0;  

这边我选取了一首非常激昂的音乐,听说恐怖的环境音跟激昂的战斗音乐更搭哦。

 

这边看到了DMusic_Load_MIDI方法,因为dx对读取做了一层封装,所以这个函数的实现没有那么复杂。我们有理由相信,随着dx版本的迭代,使用这类方法会越来越方便。

 

让我们来看看load方法:

  1. // 读取MIDI文件  
  2. int DMusic_Load_MIDI(LPWSTR filename)  
  3. {  
  4.     DMUS_OBJECTDESC objDesc;    //音乐描述  
  5.     HRESULT hr;  
  6.     IDirectMusicSegment *pSegment = NULL;   //音乐字段  
  7.   
  8.     int id = -1;  
  9.     for (int index = 0; index < DM_NUM_SEGMENTS;  index)    //查找可用的内存空间  
  10.     {  
  11.         if (dm_midi[index].state == MIDI_NULL)  
  12.         {  
  13.             id = index;  
  14.             break;  
  15.         }  
  16.     }  
  17.   
  18.     if (id == -1)  
  19.     {  
  20.         popMessage(TEXT("音乐已经放满了"));  
  21.         return -1;  
  22.     }  
  23.   
  24.     // 获取工作目录  
  25.     char szDir[_MAX_PATH];  
  26.     WCHAR wszDir[_MAX_PATH];  
  27.     if (_getcwd(szDir, _MAX_PATH) == NULL)  
  28.     {  
  29.         popMessage(TEXT("获取工作目录失败"));  
  30.         return -1;  
  31.     }  
  32.     MULTI_TO_WIDE(wszDir, szDir);  
  33.   
  34.     // 设置查询目录  
  35.     hr = dm_loader->SetSearchDirectory(GUID_DirectMusicAllTypes, wszDir, FALSE);  
  36.     if (FALSE(hr))  
  37.     {  
  38.         popMessage(TEXT("设置查找目录失败"));  
  39.         return -1;  
  40.     }  
  41.   
  42.     // 设置描述  
  43.     DDRAW_INIT_STRUCT(objDesc);  
  44.     objDesc.guidClass = CLSID_DirectMusicSegment;  
  45.     wcscpy_s(objDesc.wszFileName, filename);  
  46.     objDesc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_FILENAME;  
  47.   
  48.     // 获取音乐  
  49.     dm_loader->GetObjectW(&objDesc, IID_IDirectMusicSegment, (void**)&pSegment);  
  50.     if (FAILED(hr))  
  51.     {  
  52.         popMessage(TEXT("获取音乐失败"));  
  53.         return -1;  
  54.     }  
  55.   
  56.     //设置参数  
  57.     pSegment->SetParam(GUID_StandardMIDIFile, -1, 0, 0, (void*)dm_perf);  
  58.     pSegment->SetParam(GUID_Download, -1, 0, 0, (void*)dm_perf);  
  59.   
  60.     dm_midi[id].dm_segment = pSegment;  
  61.     dm_midi[id].dm_segstate = NULL;  
  62.     dm_midi[id].state = MIDI_LOADED;  
  63.   
  64.     return id;  
  65. }  

不要忘了游戏结束的释放:

  1. dm_perf->Stop(NULL, NULL, 0, 0); //释放音乐相关对象  
  2. for (DWORD i = 0; i < DM_NUM_SEGMENTS;  i)  
  3. {  
  4.     if(dm_midi[i].dm_segment)  
  5.     {  
  6.         dm_midi[i].dm_segment->SetParam(GUID_Unload, -1, 0, 0, (void*)dm_perf);  
  7.         dm_midi[i].dm_segment->Release();  
  8.     }  
  9. }  
  10. dm_perf->CloseDown();  
  11. dm_perf->Release();  
  12.   
  13. dm_loader->Release();  
  14.   
  15. CoUninitialize();   //释放COM对象  

最后在主循环中添加一下代码:

  1. if (count % 600 == 0)  
  2. {  
  3.     dm_perf->PlaySegment(dm_midi[now_music_id].dm_segment, 0, 0, &dm_midi[now_music_id].dm_segstate);    //播放当前的音乐  
  4. }  
  5. if (count % 600 == 300)  
  6. {  
  7.     dm_perf->Stop(dm_midi[now_music_id].dm_segment, NULL, 0, 0); //停止当前的音乐  
  8. }  
  9. count;  

每个十秒播放停止就完成了。

 

这样Windows2d的部分就完结了,说不定会试一下写个什么算法的玩玩。不过等继续这个课程应该要到3d部分了。

原文链接

著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台