waveOutOpen – 打开波形输出设备
waveOutPrepareHeader – 准备播放缓冲区
waveOutUnprepareHeader – 取消播放缓冲区
waveOutWrite – 将数据写入波形输出设备
waveOutReset – 波形输出设备复位(清除正在播放的数据,停止播放)
waveOutPause – 波形输出设备暂停(暂停播放)
waveOutRestart – 波形输出设备恢复(继续播放)
waveOutClose – 关闭波形输出设备
播放时使用的顺序大致如下:
waveOutOpen 打开设备
waveOutPrepareHeader 准备缓冲区
waveOutWrite 写入波形设备
waveOutReset 波形设备复位
waveOutClose 关闭波形设备
至于暂停就更简单,播放时执行waveOutPause时暂停播放,再执行waveOutRestart时继续播放。
现在工具已经齐备了,下来就是准备材料了。对于这个播放器来说,最重要的材料是RIFF档案、WAVEFORMATEX和WAVEHDR这三个结构。下面就简单介绍一下这三个结构:
RIFF全称为资源互换文件格式(ResourcesInterchange FileFormat),RIFF文件是windows环境下大部分多媒体文件遵循的一种文件结构,RIFF文件所包含的数据类型由该文件的扩展名来标识,能以RIFF文件存储的数据包括:音频视频交错格式数据(.AVI) 波形格式数据(.WAV) 位图格式数据(.RDI) MIDI格式数据(.RMI)调色板格式(.PAL)多媒体电影(.RMN)动画光标(.ANI)其它RIFF文件(.BND) 。具体格式如下:
WAV文件的基本格式
类型 |
内容 |
变量名 |
大小 |
取值 |
||
RIFF头 |
文件标识符串 |
fileId |
4B |
“RIFF” |
||
头后文件长度 |
fileLen |
4B |
非负整数(=文件长度-8) |
|||
数据类型标识符 |
波形文件标识符 |
waveId |
4B |
“WAVE” |
||
格式块 |
块头 |
格式块标识符串 |
chkId |
4B |
“fmt ” |
|
头后块长度 |
chkLen |
4B |
非负整数(= 16或18) |
|||
块数据 |
格式标记 |
wFormatTag |
2B |
非负短整数(PCM=1) |
||
声道数 |
wChannels |
2B |
非负短整数(= 1或2) |
|||
采样率 |
dwSampleRate |
4B |
非负整数(单声道采样数/秒) |
|||
平均字节率 |
dwAvgBytesRate |
4B |
非负整数(字节数/秒) |
|||
数据块对齐 |
wBlockAlign |
2B |
非负短整数(不足补零) |
|||
采样位数 |
wBitsPerSample |
2B |
非负短整数(PCM时才有) |
|||
扩展域大小 |
wExtSize |
2B |
非负短整数 |
可选(根据chkLen=16 or 18判断) |
||
扩展域 |
extraInfo |
extSize B |
扩展信息 |
|||
数据块 |
块头 |
数据块标识符串 |
chkId |
4B |
“data” |
|
头后块长度 |
chkLen |
4B |
非负整数 |
|||
块数据 |
波形采样数据 |
x或xl、xr |
chkLen B |
左右声道样本交叉排列 样本值为整数(整字节存储,不足位补零), 整个数据块按blockAlign对齐 |
注意:波形声音档案以文字字串「RIFF」开始,用来标识这是一个 RIFF 档案。字串後面是一个 32 位元的资料块大小,表示档案其余部分的大小,或者是小於 8位元组的档案大小。 资料块以文字字串「WAVE」开始,用来标识这是一个波形声音块,後面是文字字串「fmt」——注意用空白使之成为 4 字元的字串——用来标识包含波形声音资料格式的子资料块。「fmt」字串的後面是格式资讯大小,这里是 16 位元组。格式资讯是 WAVEFORMATEX 结构的前 16 个位元组,或者,像最初定义时一样,是包含 WAVEFORMAT 结构的 PCMWAVEFORMAT 结构,其定义如下:
typedef struct pcmwaveformat - tag
{
WAVEFORMAT wf ; /*音频波形格式结构*/
WORD wBitsPerSample; /* 采样大小 */
} PCMWAVEFORMAT;
typedef struct waveformat - tag
{
WORD wFormatTag ; /* 指定格式类型; 默认 WAVE_FORMAT_PCM = 1; */
WORD nChannels;/* 指出波形数据的声道数; 单声道为 1, 立体声为 2 */
DWORD nSamplesPerSec;/* 指定采样频率(每秒的样本数) */
DWORD nAvgBytesperSec;/* 指定数据传输的传输速率(每秒的字节数) */
WORD nBlockAlign;/* 指定块对齐块对齐是数据的最小单位 */
} WAVEFORMAT; /*音频波形格式结构*/
格式资讯的後面是文字字串「data」,然後是 32 位元的资料大小,最後是波形资料本身。 用於读取标记档案的一个重要规则是忽略不准备处理的资料块。
音频波形扩展格式结构WAVEFORMATEX用于打开音频设备,其定义如下:
音频数据块缓存结构WAVEHDR 其声明如下: type struct{ LPSTR lpData; /* 指向锁定的数据缓冲区的指针 */
DWORD dwBufferLength; /* 数据缓冲区的大小 */
DWORD dwBytesRecorded; /* 录音时指明缓冲区中的数据量 */
DWORD dwUser; /* 用户数据 */
DWORD dwFlag; /* 提供缓冲区信息的标志 */
DWORD dwLoops; /* 循环播放的次数 */
struct wavehdr_tag *lpNext; /* 保留 */
DWORD reserved; /* 保留 */
} WAVEHDR;
dwFlags中提供缓冲区信息的标志。定义以下值:
WHDR_DONE被设备驱动程序设置,用来标识它是完成的(缓冲区)并且正在返回它到应用程序 WHDR_PREPARED由Windows设置表明,在缓冲区已准备waveInPrepareHeader或waveOutPrepareHeader功能。 WHDR_BEGINLOOP这个缓冲区是在第一个循环缓冲区。这个标志仅用于输出缓冲器。 WHDR_ENDLOOP这个缓冲区是在一个循环中的最后一个缓冲区。这个标志仅用于输出缓冲器。 WHDR_INQUEUE由Windows设置为显示缓冲区排队播放。
现在材料、工具都有了,接下来就是如何炒的问题了。windows程序设计是一种以物件为导向的创作,所谓物件就是程式与资料的组合,你所见到的程序界面就是一种物件。你可以先在纸上画出程序 的界面,然后再根据它逐步添加各个模块功能。下面就是播放器的界面:
{
//显示错误信息
return TRUE ;
}
//将当前播放文件加亮
//开启音频缓冲线程
return TRUE ;
//处理暂停播放消息
case IDC_PAUSE:
if//如果暂停条件为真
{
//继续播放
}
else//如果暂停条件为假
{
//暂停播放
}
return TRUE ;
//处理上一首消息
case IDC_LAST:
//如果播放的是第一首则不处理
//发送WM_WAVEPLAY消息
return TRUE ;
//处理下一首消息
case IDC_NEXT:
//如果播放的是最后一首则不处理
//发送WM_WAVEPLAY消息
return TRUE ;
//处理正常播放消息
case IDC_NORMAL:
//将标志置为正常播放
return TRUE ;
//处理单次循环消息
case IDC_REPLAY_ONE:
//将标志置为单次循环
return TRUE ;
//处理全部循环消息
case IDC_REPLAY_ALL:
//将标志置为全部循环
return TRUE ;
//处理停止消息
case IDC_STOP:
//设置停止标志
//发送WM_WAVEPLAY消息
return TRUE ;
//处理列表控件消息
case IDC_LIST:
if(HIWORD(wParam) == LBN_DBLCLK)//如果双击文件
{
//设置线程关闭条件为真
//重置波形设备
//解除所有的WAVEHDR结构
//关闭波形设备
//如果存在打开文件句柄则关闭它
//取得双击的文件序号
//获取wave文件音频格式结构
//打开波形设备
//开启音频缓冲线程
}
return TRUE ;
}
break ;
//处理自定义消息
case WM_WAVEPLAY:
//设置线程关闭条件为真
//重置波形设备
//解锁所有的WAVEHDR结构
//关闭波形设备
//如果存在文件句柄则关闭它
//如果停止条件为真则退出处理
//如果单次循环条件为真
{
//代码
}
//如果全部循环条件为真
{
//代码
}
//如果播放上一首条件为真
{
//代码
}
//如果播放下一首条件为真
{
//代码
}
//如果播放的是最后一首则不处理
//获取wave文件音频格式结构
//打开波形设备
//发送列表框控件当前选择加亮消息
//开启音频缓冲线程
return TRUE ;
//处理系统控件消息
case WM_SYSCOMMAND:
switch (wParam)
{
case SC_CLOSE://处理窗口关闭消息
if (音频设备打开)
{
//设置线程关闭条件为真
//重置波形设备
//解锁所有的WAVEHDR结构
//释放所有的音频缓冲块
//关闭波形设备
//如果文件句柄存在则关闭它
//结束对话方块
}
else
{
//释放所有的音频缓冲块
//如果文件句柄存在则关闭它
//结束对话方块
}
return TRUE ;
}
break ;
}
return FALSE ;//返回假值
}
对于程序设计来说,最重要的就是逻辑。具体到这个播放器,就是各个模块的逻辑以及它们之间的联系。比如,按下打开按钮时,弹出打开文件对话框,然后读取文件名,打开设备,锁定缓冲,再进行播放,然后循环,直到所有文件播放完毕。除了各个模块自身的消息外,这个程序设计了一个自定义消息WM_WAVEPLAY,利用它对上一首、下一首、单次循环、全部循环、停止等按钮消息进行处理,这就大大缩减了代码,使逻辑结构更加清晰。逻辑搞清楚了,接下来就是最后一步了。将各个模块的代码填充完整,这个程序就基本完工了,剩下的就是调试工作了。
不过这里还有两个问题需要注意,一个是打开音频设备要用回调函数处理,另一个就是多线程技术。首先说一下打开音频设备的问题,请看一看上面的红色代码:
if(waveOutOpen(&(params.hWaveOut), WAVE_MAPPER, &wfx, (DWORD)waveOutProc,
(DWORD)&waveFreeBlockCount, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
{
//显示错误信息
//定义音频缓冲线程函数
VOID bufThread(PVOID pvoid)
{
DWORD readBytes ;//定义一个储存读取文件字节数的DWORD变量
char buffer[BLOCK_SIZE] = {'\0'} ; /* 定义一个临时缓冲 */
volatile PPARAMS pparams ;//定义一个参数结构指针
waveFreeBlockCount = BLOCK_COUNT; //设置空闲缓冲块的总数量
waveCurrentBlock= 0;//设置当前缓冲块为第一块
pparams = (PPARAMS)pvoid ;//给参数结构指针赋值
if(pparams->bShutoff)//判断线程关闭条件
return ;
//移动文件指针到音频数据起始地址
if(INVALID_SET_FILE_POINTER == SetFilePointer(pparams->hFile,
pparams->iCount,NULL,FILE_BEGIN))
{
MessageBox(NULL,TEXT("指针神经病!"),
TEXT("提示"),MB_OK | MB_ICONWARNING);
return ;
}
//在循环中读取音频数据到临时缓冲,再写入波形设备
while(!pparams->bShutoff) //判断线程关闭条件
{
//读取数据到临时缓冲
if(!ReadFile(pparams->hFile, buffer, sizeof(buffer), &readBytes, NULL))
break ;
//如果数据为零则退出循环
if(readBytes == 0)
break;
//如果读取的结果小于临时缓冲,则填入0补充完整
if(readBytes < sizeof(buffer))
memset(buffer + readBytes, 0, sizeof(buffer) - readBytes);
//将音频数据写入波形设备进行播放
writeAudio(pparams->hWaveOut, buffer,sizeof(buffer));
}
//关闭文件
CloseHandle(pparams->hFile);
if(pparams->bShutoff)//判断线程关闭条件
return ;
//如果线程关闭条件为假且空闲缓冲块数量小于全部缓冲块数量则等待
while(!pparams->bShutoff && waveFreeBlockCount < BLOCK_COUNT)
Sleep(0) ;//线程交出自己的时间片段给别的线程
if(!pparams->bShutoff)//如果线程关闭条件为假则发送用户自定义消息
PostMessage(pparams->hwnd,WM_WAVEPLAY,0,0) ;
return ;
}
在建立多执行绪的 Windows 程式时,需要在「Project Settings」对话方块中做一些修改。选择「C/C++」页面标签,然後在「Category」下拉式清单方块中选择「Code Generation」。在「Use Run-Time Library」下拉式清单方块中,可以看到用於「Release」设定的「Single-Threaded」和用於 Debug 设定的「Debug Single-Threaded」。将这些分别改为「Multithreaded」和「Debug Multithreaded」。这将把编译器旗标改为/MT,它是编译器在编译多执行绪的应用程式所需要的。 具体地说, 编译器将在.OBJ 档案中插入 LIBCMT.LIB 档案名,而不是 LIBC.LIB。连结程式使用这个名称与执行期程式库函式连结。 LIBC.LIB 和 LIBCMT.LIB 档案包含 C 语言程式库函式, 有些 C 语言程式库函式包含静态资料。例如,由於 strtok 函式可能被连续地多次呼叫,所以它在静态记忆体中储存了一个指标。在多执行绪程式中,每个执行绪必须在 strtok 函式中有它自己的静态指标。因此,这个函式的多执行绪版本稍微不同於单执行绪的 strtok 函式。 同时请注意,必须包含表头档案 PROCESS.H,这个档案定义一个名为_beginthread 的函式,它启动一个新的执行绪。只有定义了_MT 识别字,才会宣告这个函式,这是/MT 旗标的另一个结果。下面简单介绍一下_beginthread函数:
beginthread
uintptr_t _beginthread(
void( *start_address )( void * ),
unsigned stack_size,
void *arglist
);
Parameters 参数:
start_address:程序执行一个新线程的起始地址,即线程函数的名称。
Start address of a routine that begins execution of a new thread. For _beginthread, the calling convention is either __cdecl or __clrcall; for _beginthreadex, it is either __stdcall or __clrcall.
stack_size:新线程的堆栈大小或0值。
Stack size for a new thread or 0.
Arglist:传给新线程的变量清单或空值。
Argument list to be passed to a new thread or NULL.
Return Value 返回值:
如果新线程建立成功,函数返回该线程的句柄;然而,如果新线程退出太快,_beginthread函数可能返回一个有误的句柄。_beginthread发生错误时返回1L。