Windows编程 DirectSound DirectMusic 音效和音乐

来源:互联网 发布:java 写html table 编辑:程序博客网 时间:2024/05/16 15:54

版本:VS2015 语言:C++

 

最近项目遭遇变故,公司随意开人,导致原本要出包的项目现在乱七八糟。现在不知道未来会怎么样……

 

我们Windows上2D的编程今天是最后一课了,没有听错,2D完结,接下来就是3D了,书已经准备好了,就等我回去去取,有没有点小兴奋呢?嘛,我现在是还没有接触过3D引擎啦,不过等学完再去看看Unreal岂不也是美哉。

 

今天要讲的是在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 1const 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;//文件指针1UCHAR* audio_ptr_2 = NULL;//文件指针2DWORD 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数据部分// 使用定义的文件格式,将指针指到riffparent.fccType = mmioFOURCC('W', 'A', 'V', 'E');//设置文件的媒体格式,当然是WAV啦if (mmioDescend(hwav, &parent, NULL, MMIO_FINDRIFF)){mmioClose(hwav, 0);popMessage(TEXT("WAV 指针移动失败了"));return -1;}// 指针移动到fmtchild.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;//当前播放音乐的idint count = 0;//计数

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

 

首先看看初始化:

// 初始化COMif (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部分了。

 

有空再见。


1 0
原创粉丝点击