Windows编程 DirectSound DirectMusic 音效和音乐
今天要讲的是在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; //dx音效对象
- DSBUFFERDESC dsbd; //音效缓存描述
- // WAV文件读入后存放的类
- 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方法。
下面我们来看一下:
- // 读取WAV文件
- int DSound_Load_WAV(LPWSTR filename, bool is_default = true)
- {
- // 使用windows的mmio来读取文件(使用这些类请先包含mmsystem.h头文件)
- // 初始化
- HMMIO hwav; //文件句柄
- MMCKINFO parent; //父类块
- MMCKINFO child; //子类块
- WAVEFORMATEX wfmtx; //文件信息结构
- int sound_id = -1; //音效的3唯一标识
- int index;
- UCHAR* snd_buffer; //文件数据缓存
- UCHAR* audio_ptr_1 = NULL; //文件指针1
- UCHAR* audio_ptr_2 = NULL; //文件指针2
- 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;
- // 打开文件到hwav中
- if ((hwav = mmioOpen(filename, NULL, MMIO_READ | MMIO_ALLOCBUF)) == NULL)
- {
- popMessage(TEXT("打开WAV文件失败"));
- return -1;
- }
- // 注意:一个WAV文件由3部分组成:声纹riff标准格式、wavfmt文件标识、data数据部分
- // 使用定义的文件格式,将指针指到riff
- parent.fccType = mmioFOURCC('W', 'A', 'V', 'E'); //设置文件的媒体格式,当然是WAV啦
- if (mmioDescend(hwav, &parent, NULL, MMIO_FINDRIFF))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 指针移动失败了"));
- return -1;
- }
- // 指针移动到fmt
- child.ckid = mmioFOURCC('f', 'm', 't', ' ');
- if (mmioDescend(hwav, &child, &parent, 0))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV FMT 指针移动失败了"));
- return -1;
- }
- // 读取wav格式信息
- if (mmioRead(hwav, (char*)&wfmtx, sizeof(wfmtx)) != sizeof(wfmtx))
- {
- mmioClose(hwav, 0);
- popMessage(TEXT("WAV 信息格式读取失败了"));
- return -1;
- }
- if (wfmtx.wFormatTag != WAVE_FORMAT_PCM) //确定格式是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;
- // 下面这里是dxSound的部分,创建缓存(相当于ddraw中的表面),供声音对象使用
- // 声音信息结构
- WAVEFORMATEX pcmwf;
- memset(&pcmwf, 0, sizeof(WAVEFORMATEX));
- pcmwf.wFormatTag = WAVE_FORMAT_PCM; //WAV格式
- pcmwf.nChannels = 1; //单声道
- pcmwf.nSamplesPerSec = 11025; //采样频率频率
- pcmwf.nBlockAlign = 1; //采样位数(这里表明一个字节)
- pcmwf.nAvgBytesPerSec = pcmwf.nSamplesPerSec * pcmwf.nBlockAlign; //每秒采样的字节数
- pcmwf.wBitsPerSample = 8; //采样位数(这里表明8bit)
- 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); //将两块对应的数据拷贝到指针中(算是sound_fx的dsbuffer中)
- 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; //dx音乐表演对象,是不是感觉有点奇怪,不用惊讶,音乐对象(IDirectMusic)也是有的,会在表演对象创建的时候在后台创建,你不会接触到他。
- IDirectMusicLoader *dm_loader = NULL; //音乐加载器
- // MIDI文件读取后存放的类
- 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; //当前播放音乐的id
- int count = 0; //计数
接下来我们要实现一个什么样的效果呢?我打算一开始播放音乐,隔十秒后停止,然后再隔十秒再播放,再隔十秒再停止,如此反复。嘛,这就是我要定义计数的原因了。
首先看看初始化:
- // 初始化COM
- if (FAILED(CoInitialize(NULL)))
- {
- popMessage(TEXT("COM初始化失败"));
- return 0;
- }
- // 初始化音乐
- if (FAILED(CoCreateInstance(CLSID_DirectMusicPerformance, NULL, CLSCTX_INPROC, IID_IDirectMusicPerformance, (void**)&dm_perf))) //稍微有点诡异的创建方法,而且为什么要这么多id。。。
- {
- popMessage(TEXT("音乐表演对象创建失败"));
- return 0;
- }
- if (FAILED(dm_perf->Init(NULL, lpds, main_window_handle))) //表演对象初始化,注意传入音效对象,如果没有音效对象的话直接传NULL,但是没有音效的游戏额,就像美食没有嚼劲,干瘪瘪的摊在舌头牙齿上。
- {
- 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方法:
- // 读取MIDI文件
- 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(); //释放COM对象
最后在主循环中添加一下代码:
- 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部分了。