PlaySound("test.wav",NULL,SND_FILENAME|SND_ASYNC); |
这种混音的方式效果跟你采用的算法有很大关系,但是如果我们采用Directsound进行混音,就简单多了,我们只需要将我们要混音的内容传给它,Directsound会在内部自动进行混音的。下面我们就进入Directsound混音编程。
在了解Directsound如何混音前我们先来看看DirectSound是如何播放一段wave音频的。
这里只是简单的介绍一下播放声音的步骤。
第一步,创建一个设备对象。
在你的代码中你可以通过调用DirectSoundCreat8函数来创建一个支持IDirectSound8接口的对象,这个对象通常代表缺省的播放设备。当然你可以枚举可用的设备,然后将设备的GUID传递给DirectSoundCreat8函数。
注意,Directsound虽然基于COM,但是你并不需要初始化com库,这些Directsound都帮你做好了,当然,如果你使用DMOs特技,你就要自己初始化com库了,切记。
第二步,创建一个辅助Buffer,也叫后备缓冲区
你可以通过IDirectSound8::CreateSoundBuffer来创建buffer对象,这个对象主要用来获取处理数据,这种buffer称作辅助缓冲区,以和主缓冲区区别开来,Direcsound通过把几个后备缓冲区的声音混合到主缓冲区中,然后输出到声音输出设备上,达到混音的效果。
第三步,获取PCM类型的数据
将WAV文件或者其他资源的数据读取到缓冲区中。
第四步,将数据读取到缓冲区
你可以通过 IDirectSoundBuffer8::Lock.方法来准备一个辅助缓冲区来进行写操作,通常这个方法返回一个内存地址,见数据从你的私人buffer中复制到这个地址中,然后调用IDirectSoundBuffer8::Unlock.
第五步,播放缓冲区中的数据
你可以通过IDirectSoundBuffer8::Play方法来播放缓冲区中的音频数据,你可以通过IDirectSoundBuffer8::Stop来暂停播放数据,你可以反复的莱停止,播放,音频数据,如果你同时创建了几个buffer,那么你就可以同时来播放这些数据,这些声音会自动进行混音的。
看到了吧,Directsound混音很简单,我们只要在一个设备上创建几个辅助的缓冲区,然后将数据读取到缓冲区中,同时的播放,Directsound就会自动在主缓冲区自动混音的。至于同时可以播放几个辅助缓冲区则有硬件设备的性能决定。
在WDM驱动模式下,混音的工作由核心混音器来完成,不同的辅助缓冲区可能具有不同的WAV格式(例如,不同的采样频率),在必要的时候,辅助缓冲区的格式要转换成主缓冲区,或者核心混音器的格式。
在VXD驱动模式下,如果你的辅助缓冲区都采用相同的音频格式,并且硬件的音频格式也和你的音频格式匹配,此时,混音器不用作任何的转换。你的应用程序可以创建一个主缓冲区,然后通过IDirectSoundBuffer8::SetFormat来设置硬件的输出格式。要注意,只有你的协作度一定要是Priority Cooperative Level.,并且,一定要创建辅助缓冲区前设置主缓冲区,DirectSound会将你的设置保存下来。
在WDM模式下,对主缓冲区的的设置没有作用,因为主缓冲区的格式是由内核混音器来决定的。
下面开始吧,让我们看看如何进行混音吧,假设我们的背景需要混音的素材是下面的三个wave文件,"test1 .wav" "test2.wav" "test3.wav"。首先定义一下我们需要的几个变量:
LPDIRECTSOUND8 g_pDS = NULL; LPDIRECTSOUNDBUFFER g_pDsbuffer[3] = NULL; CWaveFile* g_pWaveFile;// WAVEFORMATEX g_wfxInput; //输入的音频格式 |
这里简单介绍一下CWaveFile类,Directsound里封装了一个CWaveFile类用来操作wav文件,可以通过open来写入文件的头信息,write来写入文件的数据,Getsize函数获取文件的长度,close关闭文件。你可以在DirectSound的路径下找到这个类的定义(SDK root)/samples/C++/Common/Src/Dsutil.cpp。
首先初始化Directsound
BOOL InitDirectSound() { if ( FAILED( hr = DirectSoundCreate(NULL, & g_pDS, NULL ) ) ) return FALSE; // Set cooperative level. if ( FAILED( hr = g_pDS ->SetCooperativeLevel( hwnd, DSSCL_PRIORITY ) ) ) return FALSE; return TRUE; } |
在初始化Directsound的时候,创建设备对象最简单的方法就是通过DirectSoundCreate8函数,这个函数的第一个参数指定了和这个对象邦定的设备的GUID,你可以通过枚举设备来获取这个设备的GUID, 如果这个参数也可以为NULL,缺省的系统的声音输出设备就是DSDEVID_DefaultPlayback 。当你创建完设备对象后,一定要调用IDirectSound8::SetCooperativeLevel来设置协作度,否则,你不会听到声音的。DirectSound定义了三种水平,DSSCL_NORMAL, DSSCL_PRIORITY, and DSSCL_WRITEPRIMARY
在Priority层次的协作度下,应用程序可以有优先权使用硬件资源,比如使用硬件进行混音,当然也可以设置主缓冲区的媒体格式,游戏程序应该采用这个层次的协作度,这个层次的协作度在允许应用程序控制采用频率和位深度的同时,也给应用程序很大的权力,这个层次的协作度允许其他应用程序的声音和游戏的音频同时被听到,不影响。
下面的函数加载wave文件,然后将音频数据读取到缓冲区中,然后通过Directsound创建了的静态辅助缓冲区,将音频数据copy到Directsound的静态辅助缓冲区,然后就可以play了。
LPDIRECTSOUNDBUFFER LoadWaveFile(LPSTR lpzFileName) { DSBUFFERDESC dsbdesc; HRESULT hr; BYTE *pBuffer; DWORD dwSizeRead; LPDIRECTSOUNDBUFFER lpdsbStatic=NULL; if( FAILED( hr = g_pWaveFile->Open( lpzFileName, &g_wfxInput, WAVEFILE_WRITE ) ) ) { return NULL; } DWORD dwSize = g_pWaveFile->GetSize(); pBuffer = new BYTE[dwSize]; g_pWaveFile->Read(pBuffer,dwSize,&dwSizeRead); if(dwSizeRead > 0) { memset(dsbdesc,0,sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); dsbdesc.dwFlags =DSBCAPS_STATIC; dsbdesc.dwBufferBytes =dwSizeRead; dsbdesc.lpwfxFormat = g_wfxInput; if ( FAILED( g_pDS->CreateSoundBuffer(&dsbdesc, & lpdsbStatic, NULL ) ) ) { g_pWaveFile->Close(); delete pBuffer; return NULL } LPVOID lpvWrite; DWORD dwLength; if (DS_OK == lpdsbStatic ->Lock( 0, // Offset at which to start lock. 0, // Size of lock; ignored because of flag. &lpvWrite, // Gets address of first part of lock. &dwLength, // Gets size of first part of lock. NULL, // Address of wraparound not needed. NULL, // Size of wraparound not needed. DSBLOCK_ENTIREBUFFER)) // Flag. { memcpy(lpvWrite, pBuffer, dwLength); lpdsbStatic ->Unlock( lpvWrite, // Address of lock start. dwLength, // Size of lock. NULL, // No wraparound portion. 0); // No wraparound size. } } delete pBuffer; return lpdsbStatic; } |
假设你锁定了30,000字节,偏移位置为20,000字节,也就是开始位置,如果你的缓冲区的大小为40,000字节,此时就会给你返回四个数据:
1、内存地址的偏移位置20,000,
2、从偏移位置到buffer的最末端的字节数,也是20,000,你要在第一个地址写入20,000个字节的内容
3、偏移量为0的地址
4、从起始点开始的字节数,也就是10,000字节,你要将这个字节数的内容写入第二个地址。
如果不包含零点,最后两个数值为NULL和0,
当然,你也有可能锁定buffer的全部内存,建议你在播放的时候不要这么做,通过你只是更新所有buffer中的一部份,例如,你可能在播放广标到达1/2位置前要将第一个1/4内存更新成新的数据,你一定不要更新play光标和Write光标间的内容。
下面的这个函数演示了如果向streaming buffer中填充音频数据,在调用这个函数之前,你一定要确保你的streaming buffer是空的,但如何知道buffer是空闲没有数据呢?一个更有效的方法采用通知机制,通过IDirectSoundNotify8::SetNotificationPositions方法,你可以设置任何一个小于buffer的位置来触发一个事件,然后响应处理函数中调用下面的函数将音频数据copy到Directsound的Streaming buffer中。
BOOL AppWriteDataToBuffer( LPDIRECTSOUNDBUFFER8 lpDsb, // The buffer. DWORD dwOffset, // Our own write cursor. LPBYTE lpbSoundData, // Start of our data. DWORD dwSoundBytes) // Size of block to copy. { LPVOID lpvPtr1; DWORD dwBytes1; LPVOID lpvPtr2; DWORD dwBytes2; HRESULT hr; // Obtain memory address of write block. This will be in two parts // if the block wraps around. hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1,&dwBytes1, &lpvPtr2, &dwBytes2, 0); // If the buffer was lost, restore and retry lock. if (DSERR_BUFFERLOST == hr) { lpDsb->Restore(); hr = lpDsb->Lock(dwOffset, dwSoundBytes, &lpvPtr1, &dwBytes1,&lpvPtr2, &dwBytes2, 0); } if (SUCCEEDED(hr)) { // Write to pointers. CopyMemory(lpvPtr1, lpbSoundData, dwBytes1); if (NULL != lpvPtr2) { CopyMemory(lpvPtr2, lpbSoundData+dwBytes1, dwBytes2); } // Release the data back to DirectSound. hr = lpDsb->Unlock(lpvPtr1, dwBytes1, lpvPtr2,dwBytes2); if (SUCCEEDED(hr)) { // Success. return TRUE; } } // Lock, Unlock, or Restore failed. return FALSE; } |
void Play(LPDIRECTSOUNDBUFFER lpdsbStatic) { if ( lpdsbStatic == NULL ) return; lpdsbStatic->SetCurrentPosition(0); lpdsbStatic->Play(0,0,0); } |
Void StartMixer() { InitDirectSound(); g_pDsbuffer[0] = LoadWaveFile("test1.wav"); g_pDsbuffer[1] = LoadWaveFile("test2.wav"); g_pDsbuffer[2] = LoadWaveFile("test3.wav"); Play(g_pDsbuffer[0]); Play(g_pDsbuffer[1]); Play(g_pDsbuffer[2]); } |