VC中使用低级音频函数WaveX播放声音文件
王结太 2004.08.08
---------------------------------------------------------------------------------------------------------------------
文章摘要:
本文讨论并实现了在VC++中使用低级音频函数WaveX播放声音文件的方法。
---------------------------------------------------------------------------------------------------------------------
Windows通过高级音频函数、媒体控制接口MCI设备驱动程序;低级音频函数MIDI Mapper、低级音频设备驱动;以及DirectSound提供了音频服务,可以从声卡获取音频流。
1. 播放声音文件的其它方法
在介绍wavex系列之前,我先来介绍之外的其它几种方法:
1.1 MCI方法简介
用MCI方法是很方便的,它对媒体设备控制主要通过命令接口函数mciSendCommand()或者字符串接口函数mciSendString()来完成的,这两个函数的作用相同。命令接口函数比命令字符串使用起来要复杂,但它为MCI提供了更为强大的控制能力,两个接口函数的原型:
MCIERROR mciSendCommand(MCIDEVICEID IDDevice,UINT uMsg,DWORD fdwCommand,DWORD dwParam);
MCIERROR mciSendString(LPCTSTR lpszCommand, LPTSTR lpszReturnString, UINT cchReturn, HANDLE hwndCallback);
比如要使用mciSendCommand方法,我们先在MCI_OPEN_PARMS中设置要播放的文件并发送MCI_OPEN命令打开声音设备,发送MCI_PLAY命令消息播放,结束后发送MCI_STOP命令关闭设备。关于它们的具体使用方法可以参考MSDN。
1.2 PlaySound方法
BOOL sndPlaySound(LPCSTR lpszSound, UINT fuSound );
BOOL PlaySound(LPCSTR pszSound,HMODULE hmod, DWORD fdwSound);
其中参数lpszSound是需要播放声音的.WAV文件的路径和文件名,hmod在这里为NULL,fuSound是播放声音的标志,详细说明请参考VC++中的帮助。 例如播放C:\sound\music.wav可以用sndPlaySound ("c:\\sound\\music.wav",SND_ASYNC);或PlaySound("c:\\sound\\music.wav",NULL, SND_ASYNC|SND_NODEFAULT );如果没有找到music.wav文件,第一种格式将播放系统默认的声音,第二种格式不会播放系统默认的声音[1],这是SND_NODEFAULT标志的作用。
当然我们也可以将声音文件作为用户自定义资源加入程序资源文件中,经过编译连接生成EXE文件,这样就可以实现无.WAV文件的声音播放。利用上面的函数也很简单,如下,其中IDR_YOUR_WAVE是加入的wave文件资源标识符:
PlaySound(MAKEINTRESOURCE(IDR_YOUR_WAVE),GetModuleHandle(NULL), SND_RESOURCE);
2. 使用低级音频函数WaveX
下面将进入文章的主题。
2.1 概述
低层音频服务及重要的数据结构低级音频服务控制着不同的音频设备,这些设备包括WAVE、MIDI和辅助音频设备[2]。低级音频服务包括如下内容:(1)查询音频设备;(2)打开和关闭设备驱动程序;(3)分配和准备音频数据块;(4)管理音频数据块;(5)应用MMTIME结构;(6)处理错误。
2.2 重要消息及数据结构
使用低级音频函数之所以能够对各个声音数据块操作,要归功于Windows的消息映射,Windows在采集、播放完一个数据块之后就会发送有关的消息。播放声音涉及到的重要消息及触发条件如下:
MM_WOM_CLOSE:在一个波形声音输出设备关闭时发出,之后该设备句柄不再有效
MM_WOM_DONE:当给定的输出缓存播放完毕返回给应用程序,或者直接调用waveOutReset函数停止播放并重置管理器
MM_WOM_OPEN:当给定的波形声音输出设备被打开时
MOM_CLOSE:当MIDI输出设备关闭时
WOM_DONE:当留缓冲播放完毕并正被返回程序时发到MIDI输出回调函数
WOM_OPEN:当MIDI输出设备打开时
重要的数据结构:
波形数据格式 WAVEFORMAT/WAVEFORMATEX
波形数据缓冲区格式 WAVEHDR
音频输出设备性能 WAVEOUTCAPS
这些内容都定义在mmsystem.h头文件中,更为具体的信息请参阅MSDN。
2.3 wavex播放声音波形文件方法的大致流程
常用mmio函数:
mmioOpen( ) 打开一个RIFF文件
mmioDescend ( ) 进入块
mmioRead( ); 该取RIFF文件
mmioAscend ( ); 跳出块
mmioClose( ); 关闭PIFF文件
对于块来说,进入块和跳出块是配对的。
读取WAV文件的读取过程:
mmioOpen( ) 打开文件
↓
mmioDescend ("WAVE") 进入"fmt"块
↓
mmioRead( ) 读取WAVE文件格式信息
↓
mmioAscend ( ) 跳出"fmt"块
↓
mmioDescend ("data") 进入"data"块
↓
mmioRead( ) 读取WAVE数据信息
↓
mmioClose( ) 关闭文件。
输出WAV文件的过程:
WaveOutOpen () 打开一个输出设备
↓
WaveOutPrepareHeader() 准备WAVE数据头。
↓
WaveOutWrite() 将数据写入设备并开始播放
↓
WaveOutReset() 停止播放并重置管理器
↓
WaveOutClose() 并闭播放设备
↓
WaveOutUnpareHeader() 清理用WaveOutPrepareHeader准备的Wave
2.4 主要程序清单
2.4.1 播放部分
void CPlayWaveDlg::OnPlay()
{
LPSTR szFileName;//声音文件名
LPSTR szPathName;
MMCKINFO mmckinfoParent;
MMCKINFO mmckinfoSubChunk;
DWORD dwFmtSize;
DWORD m_WaveLong;
WAVEFORMATEX* lpFormat;
DWORD m_dwDataOffset;
DWORD m_dwDataSize;
WAVEOUTCAPS pwoc;
LONG lSoundOffset;
LONG lSoundLong;
CEdit* pEdit = (CEdit*) GetDlgItem(IDC_FILE);
pEdit->GetWindowText(m_strFileName);
if (m_strFileName == "")
{
ShowMsg("Please select a wave file to play!");
return;
}
szPathName = m_strPathName.GetBuffer(0);
szFileName = m_strFileName.GetBuffer(0);
//打开波形文件
if (!(m_hmmio = mmioOpen(szPathName, NULL, MMIO_READ | MMIO_ALLOCBUF)))
{
/*-------------------------------------------------------------------------------
信息显示函数ShowMsg():
void CPlayWaveDlg::ShowMsg(char* szMsg, ...)
{
va_list vl;
char szBuf[256];
va_start(vl, szMsg);
vsprintf(szBuf, szMsg, vl);
va_end(vl);
::MessageBox(NULL, szBuf, "WavePlayer", MB_OK | MB_ICONEXCLAMATION);
}
---------------------------------------------------------------------------------*/
ShowMsg("Failed to open file: %s", szFileName);
return;
}
//进入块,检查打开文件是否是wave文件
mmckinfoParent.fccType = mmioFOURCC('W', 'A', 'V', 'E');
if (mmioDescend(m_hmmio, (LPMMCKINFO) & mmckinfoParent, NULL,
MMIO_FINDRIFF))
{
ShowMsg("%s is an invalid wave file!", szFileName);
mmioClose(m_hmmio, NULL);
return;
}
//寻找 'fmt' 块
mmckinfoSubChunk.ckid = mmioFOURCC('f', 'm', 't', ' ');
if (mmioDescend(m_hmmio, &mmckinfoSubChunk, &mmckinfoParent,
MMIO_FINDCHUNK))
{
ShowMsg("Cannot find fmt chunk in %s!", szFileName);
mmioClose(m_hmmio, NULL);
return;
}
//获得 'fmt '块的大小,申请内存
dwFmtSize = mmckinfoSubChunk.cksize ;
m_hFormat = LocalAlloc(LMEM_MOVEABLE, LOWORD(dwFmtSize));
if (!m_hFormat)
{
ShowMsg("Alloc memory failed!");
return;
}
lpFormat = (WAVEFORMATEX *) LocalLock(m_hFormat);
if (!lpFormat)
{
ShowMsg("Lock memory failed!");
OnStop();
return;
}
if ((unsigned long) mmioRead(m_hmmio, (HPSTR) lpFormat, dwFmtSize) !=
dwFmtSize)
{
ShowMsg("Read format chunk of %s failed!", szFileName);
OnStop();
return;
}
//离开 fmt 块
mmioAscend(m_hmmio, &mmckinfoSubChunk, 0);
//寻找 'data' 块
mmckinfoSubChunk.ckid = mmioFOURCC('d', 'a', 't', 'a');
if (mmioDescend(m_hmmio, &mmckinfoSubChunk, &mmckinfoParent,
MMIO_FINDCHUNK))
{
ShowMsg("Cannot find data chunk in: %s", szFileName);
OnStop();
return;
}
//获得 'data'块的大小
m_dwDataSize = mmckinfoSubChunk.cksize ;
m_dwDataOffset = mmckinfoSubChunk.dwDataOffset ;
if (m_dwDataSize == 0L)
{
ShowMsg("%s has no data!", szFileName);
OnStop();
return;
}
//为音频数据分配内存
lpData = new char[m_dwDataSize];
if (!lpData)
{
ShowMsg("Alloc memory for wave data failed!");
OnStop();
return;
}
lSoundOffset = m_dwDataOffset;
LONG lSize = mmioSeek(m_hmmio, lSoundOffset, SEEK_SET);
if (lSize < 0)
{
ShowMsg("Seek data chunk of %s failed!", szFileName);
OnStop();
return;
}
lSoundLong = m_dwDataSize;
m_WaveLong = mmioRead(m_hmmio, lpData, lSoundLong);
if (m_WaveLong < 0)
{
ShowMsg("Read data chunk of %s failed!", szFileName);
OnStop();
return;
}
//检查音频设备,返回音频输出设备的性能
if (waveOutGetDevCaps(WAVE_MAPPER, &pwoc, sizeof(WAVEOUTCAPS)) != 0)
{
ShowMsg("waveOutGetDevCaps() failed!");
OnStop();
return;
}
//检查音频输出设备是否能播放指定的音频文件
/*----------------------------------------------------------------------------------------------
waveOutOpen函数最后三个参数的设置对消息处理方式起决定性作用,需要特别注意,通常我们用下列处理方法:
1. 使用窗口作为消息的接收者,则第四个参数设置为该窗口的句柄,则和这次播放有关的消息都将进入该窗口的消息队列,这时 第五个参数为NULL,第六个参数为CALLBACK_WINDOW,表明由窗口的过程来处理消息。
2. 直接使用回调函数来处理消息,则第四个参数设置为该回调函数的指针,则和这次播放有关的消息都将由该函数处理,这时第 五个参数为传入该函数的参数,第六个参数为CALLBACK_FUNCTION,表明由指定函数来处理消息。
3. 使用新的线程来处理消息,则第四个参数设置为该线程函数的指针,和这次播放有关的消息都将由该线程处理,这时第五个参 数为传入该函数的参数,第六个参数为CALLBACK_THREAD,表明由线程来处理消息。
4. 如果你不需要处理消息,这后面三个参数分别为NULL,NULL,CALLBACK_NULL
----------------------------------------------------------------------------------------------*/
if (waveOutOpen(&hWaveOut, WAVE_MAPPER, lpFormat, (ULONG)m_hWnd, NULL, CALLBACK_WINDOW) !=
0)
{
ShowMsg("Open the wave out devices failed!");
OnStop();
return;
}
//准备待播放的数据
pWaveOutHdr.lpData = (HPSTR) lpData;
pWaveOutHdr.dwBufferLength = m_WaveLong;
pWaveOutHdr.dwFlags = 0;
pWaveOutHdr.dwLoops = 5;
if (waveOutPrepareHeader(hWaveOut, &pWaveOutHdr, sizeof(WAVEHDR)) != 0)
{
ShowMsg("Failed to prepare the wave data buffer!");
OnStop();
}
//将数据写入设备并开始播放
if (waveOutWrite(hWaveOut, &pWaveOutHdr, sizeof(WAVEHDR)) != 0)
{
ShowMsg("Failed to write the wave data buffer");
OnStop();
}
}
2.4.2 停止播放部分
void CPlayWaveDlg::OnStop()
{
if (m_hmmio != NULL)
{
mmioClose(m_hmmio, NULL);
}
//停止播放并重置管理器
waveOutReset(hWaveOut);
//关闭播放设备
waveOutClose(hWaveOut);
//清理用WaveOutPrepareHeader准备的Wave。
waveOutUnprepareHeader(hWaveOut, &pWaveOutHdr, sizeof(WAVEHDR));
//释放内存
if (m_hFormat != NULL)
{
LocalUnlock(m_hFormat);
m_hFormat = NULL;
}
if (m_hFormat != NULL)
{
LocalFree(m_hFormat);
m_hFormat = NULL;
}
if (lpData != NULL)
{
delete[] lpData;
lpData = NULL;
}
}
2.4.3 处理消息部分:
添加消息映射:ON_MESSAGE(MM_WOM_DONE,OnMMWomDone)
void CPlayWaveDlg::OnMMWomDone(UINT wParam, LONG lParam)
{
// ShowMsg("Play finished!");
OnStop();
}
2.4.4 相关头文件
/*-----------------------------------------------------------------------------------------------------------------------
说明:本文介绍的操作函数的声明包含在mmsystem.h头文件中,因此在程序中必须用#include "mmsystem.h"语句加入头文件。同时在编译时要加入动态连接导入库winmm.lib,具体实现方法有两种:
1. 从Developer Studio的Project菜单中选择Settings,然后在Link选项卡上的Object/Library Modules控制中加入winmm.lib
2. 如下所示在代码中加入#pragma comment(lib, "winmm.lib")
-----------------------------------------------------------------------------------------------------------------------*/
#include "mmsystem.h"
#pragma comment(lib, "winmm.lib")
class CPlayWaveDlg : public CDialog
{
//省略与播放无关部分
//................
protected:
HANDLE m_hData;
HWAVEOUT hWaveOut;
WAVEHDR pWaveOutHdr;
HANDLE m_hFormat;
HPSTR lpData;//音频数据
HMMIO m_hmmio;//音频文件句柄
CString m_strPathName;
CString m_strFileName;
void ShowMsg(char *szMsg, ...);
afx_msg void OnPlay();
afx_msg void OnStop();
afx_msg void OnMMWomDone(UINT wParam, LONG lParam);
DECLARE_MESSAGE_MAP()
};
以上代码在Visual C++ 6.0 + windows 2000 pro 上通过。
3. 应用
低级音频函数能够具体的在内存中对各个声音数据块进行细节控制,比如可以通过检测声音的振幅强度进行声音采集的筛选,或者进行声音文件的剪切合并等,这就为声音文件的灵活操作提供了很好的方法;因为它能够操作声音数据块,从而也能为声音的实时传输提供有效的途径。
参考文献:
1. 李灿伟 VC++中播放声音的方法
2. 李博轩 Visuanl C++ 6.0多媒体开发指南。北京:清华大学出版社,2000年2月.71-75