版本: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 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部分了。
有空再见。

1万+

被折叠的 条评论
为什么被折叠?



