2007年12月17日星期一

MMIO编程

MMIO(MultiMedia file Input and Output多媒体文件的输入与输出)比标准操作系统的I/O服务提供更多的功能,包括缓冲区I/O、RIFF文件、内存文件和定制的存储系统等。另外,多媒体文件I/O服务还对性能敏感的应用程序进行了优化。
MMIO定义了2个数据类型和2个结构类型、并提供了18个函数和1个宏,还有7个专用消息。由于篇幅所限,本节只针对RIFF文件(主要是WAV格式)的读入,介绍其中若干常用的结构和函数。
本节先介绍RIFF文件的结构和相关定义,然后给出MMIO的若干基本I/O和RIFF I/O函数,最后编写读取WAV文件和资源的内容和信息的实例代码。
12.6.1 RIFF文件
RIFF(Resource Interchange File Format资源交换文件格式)是微软为Windows设计的一类多媒体文件格式,主要包括WAV(Waveform Audio File Format波形音频文件格式)和AVI(Audio/Video Interleaved File Format音频视频交互文件格式)两种。音频文件格式WAV已经在3.3.1中介绍过,而视频文件格式AVI也在6.2.3中介绍过。

 文件结构
RIFF文件由文件头、数据类型标识及若干块组成。块(chunk)是RIFF文件的基本结构,每个块都包括如下三个域:
 用于标识块类型的4字符代码,如’RIFF’、’LIST’和’fmt ’、’data’等;
 数据块大小的字节数
 数据域本身
RIFF文件中的块是可以嵌套的,块的数据域可含若干子块,子块又可含孙块。例如,RIFF文件本身就是一个RIFF块,其数据域可以是WAV子块或AVI子块,而WAV子块又包含格式块('fmt ‘)和数据块('data’),AVI文件的AVI子块,包含格式列表块(’LIST’)、数据列表块(’LIST’)和可选的索引块(’ind1’),……。
RIFF文件具有相同的结构:
’RIFF’<文件大小><格式类型><块>……
例如,WAV文件的结构为:
’RIFF’<文件大小>’WAVE’<格式块><数据块>
而AVI文件的结构为:
’RIFF’<文件大小>’AVI ’<格式列表块><数据列表块>[<索引块>]

为了读写和处理RIFF文件,在多媒体SDK和MMIO中,定义了若干数据类型、结构和宏。下面给出它们的定义和使用方法:

 四字符代码类型和其构造宏
为了读写和比较RIFF文件的由4字符组成的块类型标识代码,定义了一个FOURCC(FOUR Character Code,四字符代码)数据类型:
typedef DWORD FOURCC;
还定义了一个由4个字符来构造FOURCC类型值的宏mmioFOURCC:
FOURCC mmioFOURCC( // 构造四字符代码
CHAR ch0, // 四字符代码的第1个字符,typedef char CHAR;
CHAR ch1, // 四字符代码的第2个字符
CHAR ch2, // 四字符代码的第3个字符
CHAR ch3 // 四字符代码的第4个字符
);
该宏是利用MAKEFOURCC宏来定义的:
#define mmioFOURCC(ch0, ch1, ch2, ch3) MAKEFOURCC(ch0, ch1, ch2, ch3);
而MAKEFOURCC宏的定义为:
#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | \
((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ));
例如:
#define ID_RIFF mmioFOURCC('R', 'I', 'F', 'F')
#define ID_WAVE mmioFOURCC('W', 'A', 'V', 'E')
#define ID_fmt mmioFOURCC('f', 'm', 't', ' ')
#define ID_data mmioFOURCC('d', 'a', 't', 'a')
FOURCC id;
ar >> id;
if (id != ID_RIFF) ……

 块结构定义
typedef struct {
FOURCC ckid; // 块ID
DWORD cksize; // 块的字节大小
FOURCC fccType; // (RIFF和LIST块的)格式类型(如’WAVE’)
DWORD dwDataOffset; // 块的数据部分的偏移量
DWORD dwFlags; // 用于MMIO函数的标志,可取0或MMIO_DIRTY(脏)
} MMCKINFO;

12.6.2 基本与RIFF I/O函数
下面是若干通用的MMIO基本I/O函数和两个用于RIFF I/O的定位函数:

 打开/关闭文件
mmioOpen函数可以用来打开缓冲和非缓冲的I/O文件,这里的文件可以是标准文件、内存文件和定制存储系统的元素。该函数的原型为:
HMMIO mmioOpen( // 返回北打开文件的句柄,出错返回NULL
LPSTR szFilename, // 文件名串,对内存文件为NULL
LPMMIOINFO lpmmioinfo, // 指向包含额外参数的MMIOINFO结构的指针
// 一般为NULL,除非是打开内存文件,指定缓冲I/O的缓冲区大小
DWORD dwOpenFlags // 打开操作的标志位组合,取值见表12-11
);
其中,MMIOINFO结构的定义为:
typedef struct {
DWORD dwFlags; // 打开文件方式标志,取值见表12-11
FOURCC fccIOProc; // 标识文件IO过程的四字符代码,无IO过程时为NULL
LPMMIOPROC pIOProc; // 件IO过程的指针
UINT wErrorRet; // 扩展的错误值,只用于打开函数
HTASK hTask; // 局部IO过程的句柄
LONG cchBuffer; // 文件缓冲区的字节大小,对非缓冲文件为0
HPSTR pchBuffer; // 指向文件缓冲区的指针,对非缓冲文件为NULL
HPSTR pchNext; // 指向缓冲区的下一读/写位置的指针
HPSTR pchEndRead; // 指向缓冲区内的可读位置尾
HPSTR pchEndWrite; // 指向缓冲区内的可写位置尾
LONG lBufOffset; // 保留
LONG lDiskOffset; // 当前文件位置(从文件头的偏移字节数)
DWORD adwInfo[4]; // IO过程维护的状态信息
DWORD dwReserved1; // 保留
DWORD dwReserved2; // 保留
HMMIO hmmio; // 由打开函数返回的文件句柄
} MMIOINFO;
其中,HPSTR类型的定义为:
typedef char _huge * HPSTR; /* a huge version of LPSTR */
表12-11 打开文件操作的标志符号常量
=========================================================
值 含义
MMIO_ALLOCBUF 打开缓冲文件
MMIO_COMPAT 打开兼容模式文件
MMIO_CREATE 创建新文件
MMIO_DELETE 删除文件
MMIO_DENYNONE 打开共享文件
MMIO_DENYREAD 打开禁止其他过程读访问的文件
MMIO_DENYWRITE 打开禁止其他过程写访问的文件
MMIO_EXCLUSIVE 打开禁止其他过程读写访问的文件
MMIO_EXIST 判断指定文件是否存在,存在时返回TRUE
MMIO_GETTEMP 创建临时文件名
MMIO_PARSE 创建完整文件名
MMIO_READ 打开只读文件
MMIO_READWRITE 打开读写文件
MMIO_WRITE 打开只写文件
=========================================================
由mmioOpen函数打开的文件,在使用完后必须调用函数mmioClose关闭它。该关闭函数对缓冲文件,还会调用mmioFlush函数将缓冲区的数据写回磁盘。关闭函数的原型为:
MMRESULT mmioClose( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
UINT wFlags // 关闭操作标志,可取0或MMIO_FHOPEN(对非MMIO文件句柄)
);
例如:
HMMIO hmmio = mmioOpen(L"sample.wav", NULL, MMIO_READ);
…… // 使用和处理数据
mmioClose(hmmio, 0);

 读/写数据
// 从文件中读取数据
LONG mmioRead( // 返回实际读出的字节数
HMMIO hmmio, // 文件句柄
HPSTR pch, // 存放读入数据的缓冲区指针
LONG cch // 读取数据的字节数
);
// 写入数据到文件
LONG mmioWrite( // 返回实际写入的字节数
HMMIO hmmio, // 文件句柄
HPSTR pch, // 存放写入数据的缓冲区指针
LONG cch // 写入数据的字节数
);
例如:
WAVEFORMATEX wfFormat;
mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX));
又如:
Write(const char* pData, LONG lLen) {
ASSERT(m_hmmio != NULL);
return ::mmioWrite(m_hmmio, pData, lLen);
}

 定位文件
// 定位:改变文件的当前位置
LONG mmioSeek( // 返回新位置(相对于文件头的字节数),出错返回-1
HMMIO hmmio, // 文件句柄
LONG lOffset, // 改变位置的偏移量
int iOrigin // 偏移类型标志,可取值有:SEEK_CUR(从当前起算)、
// SEEK_END(从文件尾起算)、SEEK_SET(从文件头起算)

);
// 下降:进入RIFF文件中的(特定)块
MMRESULT mmioDescend( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
LPMMCKINFO lpck, // 指向接收MMCKINFO结构的缓冲区指针
LPMMCKINFO lpckParent, // 指向指定父块之MMCKINFO结构的指针
// 无父块时取为NULL
UINT wFlags // 搜索标志,可取值有:0(当前位置)、MMIO_FINDCHUNK
// (指定块标识符)、MMIO_FINDLIST(列表块内的指定标识符块)、
// MMIO_FINDRIFF(RIFF块内的指定标识符块)
);
// 上升:移出RIFF文件中的当前块
MMRESULT mmioAscend( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
LPMMCKINFO lpck, // 指向(已被mmioDescend填充的)MMCKINFO结构的指针
UINT wFlags // 保留,必须为0
);
例如:
MMCKINFO ckInfo; // 定义块信息结构变量
mmioSeek(hmmio, 12, SEEK_SET); // 定位到’fmt ‘块的开始处
mmioDescend(hmmio, &ckInfo, NULL, 0); // 获取’fmt ‘块信息
…… // 读取操作
mmioAscend(hmmio, &ckInfo, 0); // 跳出’fmt ‘块
LONG currentPosition = mmioSeek(hmmio, 0, SEEK_CUR); // 获取当前位置
又例如:
// Set the position to the begining of the file
if ( mmioSeek(hmmio, 0, SEEK_SET) == -1) {
MessageBox(L"Seek to the begining of file error!", L"Error");
return false;
}
// Find the RIFF chunk
MMCKINFO mmckRiffInfo;
mmckRiffInfo.ckid = mmioFOURCC('R','I','F','F');
if ( mmioDescend(hmmio, &mmckRiffInfo, NULL, MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to RIFF chunk error!", L"Error");
return false;
}
// Find the WAVE chunk
MMCKINFO mmckWaveInfo;
mmckWaveInfo.ckid = mmioFOURCC('W','A','V','E');
if ( mmioSeek(hmmio, -4, SEEK_CUR) == -1) {
MessageBox(L"Seek to the begining of 'WAVE' error!", L"Error");
return false;
}
if ( mmioDescend(hmmio, &mmckWaveInfo, &mmckRiffInfo,
MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to WAVE chunk error!", L"Error");
return false;
}
// Find and read the format subchunk
MMCKINFO mmckFmtInfo;
mmckFmtInfo.ckid = mmioFOURCC('f','m','t',' ');
if ( mmioSeek(hmmio, 12, SEEK_SET) == -1) {
MessageBox(L"Seek to the begining of 'fmt ' error!", L"Error");
return false;
}
if ( mmioDescend(hmmio, &mmckFmtInfo, &mmckWaveInfo,
MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to fmt chunk error!", L"Error");
return false;
}
if ( mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX))
< sizeof(WAVEFORMATEX) ) {
MessageBox(L"Read format struct error!", L"Error");
return false;
}
if ( mmioAscend(hmmio, &mmckFmtInfo, 0) != 0 ) {
MessageBox(L"Ascend from fmt chunk error!", L"Error");
return false;
}
12.6.3 读取WAV文件和资源
可以利用MMIO,来读取WAV文件和资源中的数据内容与参数信息。下面给出若干有关函数和代码段,这些内容,在后面的13.3节(DirectX声音编程)中会用到。

 读取WAV文件
函数OpenWave,用于打开WAV波形音频文件,获取文件的句柄:
// 打开WAV文件
HMMIO OpenWave(LPWSTR strFilePath) {// Open WAV file
return mmioOpen(strFilePath, NULL, MMIO_READ);
}

函数GetWaveData,利用文件句柄,来获取波形音频格式结构内容和数据块长度:
// 获取波形数据:格式结构和数据块长度
bool GetWaveData(HMMIO hmmio, WAVEFORMATEX& wfFormat,
DWORD& dwDataLen) {
// Find the RIFF chunk and WAVE form type
MMCKINFO mmckInfo;
if ( mmioDescend(hmmio, &mmckInfo, NULL, 0) != 0
|| mmckInfo.ckid != FOURCC_RIFF
|| mmckInfo.fccType != mmioFOURCC('W','A','V','E') ) {
MessageBox(L"Not a WAV file !", L"Error");
return false;
}
// Find and read the format subchunk
if ( mmioDescend(hmmio, &mmckInfo, NULL, 0) != 0
|| mmckInfo.ckid != mmioFOURCC('f','m','t',' ') ) {
MessageBox(L"fmt chunk error!", L"Error");
return false;
}
if ( mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX))
< sizeof(WAVEFORMATEX) ) {
MessageBox(L"Read format struct error!", L"Error");
return false;
}
if ( mmioAscend(hmmio, &mmckInfo, 0) != 0 ) {
MessageBox(L"Ascend from fmt chunk error!", L"Error");
return false;
}
// Find and get the size of the data subchunk
mmckInfo.ckid = mmioFOURCC('d','a','t','a');
if ( mmioDescend(hmmio, &mmckInfo, NULL, MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"data chunk error!", L"Error");
return false;
}
dwDataLen = mmckInfo.cksize;
return true;
}

函数GetWave,调用OpenWave和GetWaveData函数,由文件路径串来打开波形音频文件,获取文件句柄、格式结构和数据块长度:
// 获取波形:文件句柄、格式结构和数据块长度
bool GetWave(LPWSTR strFilePath, HMMIO& hmmio,
WAVEFORMATEX& wfFormat, DWORD& dwDataLen) {
// Open WAV file
hmmio = OpenWave(strFilePath);
if ( hmmio == NULL ) {
MessageBox(L"Open WAV file error!", L"Error");
return false;
}
// Get WAV data
if ( !GetWaveData(hmmio, wfFormat, dwDataLen) ) {
MessageBox(L"Get wave data error!", L"Error");
return false;
}
return true;
}

 读取WAV资源
为了读取WAV资源,需要调用若干SDK函数来定位资源位置、装入资源到全局内存、锁定资源并获得资源地址、获取资源大小等。下面逐个加以介绍:
// 确定指定模块中指定类型和名称之资源的位置
HRSRC FindResource( // 成功返回指定资源的信息块句柄,失败返回NULL
HMODULE hModule, // 其可执行文件包含资源之模块的句柄,NULL为当前模块
LPCTSTR lpName, // 资源名,一般取MAKEINTRESOURCE(资源ID)
LPCTSTR lpType // 指定资源类型,取值可为RT_BITMAP、RT_CURSOR、
// RT_ICON、……,对波形音频资源为L"WAVE"
);
其中,hModule也可取值为MFC的全局函数AfxGetInstanceHandle的返回值:
HINSTANCE AFXAPI AfxGetInstanceHandle( ); // 返回当前应用程序的实例句柄
// 将指定资源装入全局内存
HGLOBAL LoadResource( // 成功返回资源数据句柄,失败返回NULL
HMODULE hModule, // 模块的句柄,NULL为当前模块
HRSRC hResInfo // 资源信息块句柄
);
// 释放资源
BOOL FreeResource( // 成功返回0
HGLOBAL hglbResource // 资源数据句柄
);
// 锁定资源,获得资源地址
LPVOID LockResource( // 成功返回资源头指针,失败返回NULL
HGLOBAL hResData // 资源数据句柄
);
// 获取资源字节大小
DWORD SizeofResource( // 返回资源字节数,失败返回0
HMODULE hModule, // 模块的句柄,NULL为当前模块
HRSRC hResInfo // 资源信息块句柄
);

 读取WAV资源例子
下面利用这些SDK函数,来打开波形音频资源:
// 打开WAV资源
HMMIO OpenWave(UINT uiResID) { // Open WAV resource
HMMIO hmmio = NULL;
BYTE* m_pImageData;
DWORD m_dwImageLen;
// Find the wave resource
HRSRC hresInfo = FindResource(NULL, MAKEINTRESOURCE(uiResID),
L"WAVE");
if ( hresInfo == NULL) return NULL;
// Load the wave resource
HGLOBAL hgmemWave = LoadResource(NULL, hresInfo);
// Get wave file image
if (hgmemWave != NULL) {
// Get pointer to and length of the wave image data
m_pImageData= (BYTE*)LockResource(hgmemWave);
m_dwImageLen = SizeofResource(NULL, hresInfo);
} else return NULL;
// Set the MMIOINFO struct
MMIOINFO mmioInfo;
ZeroMemory(&mmioInfo, sizeof(MMIOINFO));
mmioInfo.pIOProc = NULL;
mmioInfo.fccIOProc = FOURCC_MEM;
mmioInfo.pchBuffer = (HPSTR)m_pImageData;
mmioInfo.cchBuffer = m_dwImageLen;
// Open MMIO memory file
hmmio = mmioOpen(NULL, &mmioInfo, MMIO_READWRITE);
if ( hmmio == NULL ) {
MessageBox(L"Open memory file error!", L"Error");
return NULL;
}
return hmmio;
}
其中,用到了一个SDK宏:
// 用0填充内存块
void ZeroMemory(
PVOID Destination, // 块头地址
SIZE_T Length // 块的字节大小
);
#define ZeroMemory RtlZeroMemory
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))

函数GetWave,调用OpenWave和GetWaveData函数,由资源ID来打开波形音频文件,获取文件句柄、格式结构和数据块长度:(与WAV文件的类似,其中的GetWaveData函数相同)
// 获取波形:文件句柄、格式结构和数据块长度
bool GetWave(UINT uiResID, HMMIO& hmmio, WAVEFORMATEX& wfFormat,
DWORD& dwDataLen) {
// Open WAV resource
hmmio = OpenWave(uiResID);
if ( hmmio == NULL ) {
MessageBox(L"Open WAV resource error!", L"Error");
return false;
}
// Get WAV data
if ( !GetWaveData(hmmio, wfFormat, dwDataLen) ) {
MessageBox(L"Get wave data error!", L"Error");
return false;
}
return true;
}