Windows编程 DirectSound DirectMusic 音效和音乐

版本: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部分了。

 

有空再见。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值