vc中用WaveOut写音乐播放器

来源:互联网 发布:netpas云墙 mac 编辑:程序博客网 时间:2024/05/21 22:53
vc中用WaveOut写音乐播放器
要炒菜的话,就得先准备工具,如锅、铲子、炉灶等。对程序来说,就是各种函数的应用。WaveOut函数在windowsAPI中属于低阶接口,用来播放的话需要用到下面几个:

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

非负整数(= 1618)

块数据

格式标记

wFormatTag

2B

非负短整数(PCM=1)

声道数

wChannels

2B

非负短整数(= 12)

采样率

dwSampleRate

4B

非负整数(单声道采样数/)

平均字节率

dwAvgBytesRate

4B

非负整数(字节数/)

数据块对齐

wBlockAlign

2B

非负短整数(不足补零)

采样位数

wBitsPerSample

2B

非负短整数(PCM时才有)

扩展域大小

wExtSize

2B

非负短整数

可选(根据chkLen=16 or 18判断)

扩展域

extraInfo

extSize B

扩展信息

数据块

块头

数据块标识符串

chkId

4B

“data”

头后块长度

chkLen

4B

非负整数

块数据

波形采样数据

xxlxr

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用于打开音频设备,其定义如下:

typedef struct
{
WORD wFormatTag; /* 指定格式类型; 默认 WAVE_FORMAT_PCM = 1; */
WORD nChannels; /* 指出波形数据的声道数; 单声道为 1, 立体声为 2 */
DWORD nSamplesPerSec; /* 指定采样频率(每秒的样本数) */
DWORD nAvgBytesPerSec; /* 指定数据传输的传输速率(每秒的字节数) */
WORD nBlockAlign; /* 指定块对齐块对齐是数据的最小单位 */
WORD wBitsPerSample; /* 采样大小 */
WORD cbSize; /* 附加信息的字节大小 */
} 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程序设计是一种以物件为导向的创作,所谓物件就是程式与资料的组合,你所见到的程序界面就是一种物件。你可以先在纸上画出程序的界面,然后再根据它逐步添加各个模块功能。下面就是播放器的界面:
如图有8个按钮和一个列表框,它们代表了程序的各个功能模块。有了界面就等于建好了房子的框架,下来只要添砖加瓦就可以了。windows程序是以消息作为驱动的,对这些功能模块的处理就是各种消息的处理,下面请看伪代码:
//对话方块回调函数
BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//处理窗口消息
switch (message)
{
case WM_INITDIALOG://处理对话方块初始化消息
//初始化代码
return TRUE ;

//处理窗口控件消息
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDC_OPEN://处理打开按钮消息

//获取文件名
//输出音乐文件列表
//将循环标志改为正常
//获取wave音频格式
//打开波形设备
if(waveOutOpen(&(params.hWaveOut), WAVE_MAPPER, &wfx, (DWORD)waveOutProc,
(DWORD)&waveFreeBlockCount, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)

{
//显示错误信息

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)

{
//显示错误信息

return TRUE ;
}

waveOutProc就是回调函数的名称,waveFreeBlockCount就是传给它的参数,CALLBACK_FUNCTION指明使用回调函数处理。回调函数的名称可以自定义,但是格式却必须如下:
static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwInstance, DWORD dwParam1,DWORD dwParam2 )
{
//处理音频设备播放时的各种消息
return ;
}
为什么要在回调函数中处理设备消息?这是因为防止设备消息会干扰到程序与外界的交互消息,比如设备在播放文件时,程序也许会收到用户输入的消息,如果同时处理设备消息,这时就有可能会导致故障,而用回调函数就不会出现这种状况,因为它是在另一线程中处理消息。
这个播放器还必须使用多线程技术,因为播放文件时需要不停读音频缓冲,这会导致界面暂时卡死(即不能与用户进行交互),只能等待播放完毕。怎么解决呢?用一个线程专门进行音频数据的缓冲与播放,而主线程用于各种用户消息的处理。请看代码:
#include <process.h> //包含多线程处理头文件
VOID bufThread(PVOID pvoid) ;//声明音频缓冲线程函数
_beginthread(bufThread,0,&params) ;//开启音频缓冲线程

//定义音频缓冲线程函数
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。连结程式使用这个名称与执行期程式库函式连结。同时请注意,必须包含表头档案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。

在使用多线程时,还需注意共享变量的互斥,这里利用了临界区域的技术。这种临界区域技术专用于多线程中共享变量的互斥使用,在任何时刻,只有一个执行绪能拥有一个临界区域。因此,一个执行绪可以进入一个临界区域,设定一个变量,然後退出临界区域。另一个使用该变量的执行绪在存取变量中的项目之前也要先进入该临界区域,然後再退出临界区域。请看代码:
static CRITICAL_SECTION waveCriticalSection;//定义一个临界区域
InitializeCriticalSection(&waveCriticalSection);//初始化临界区域
EnterCriticalSection(&waveCriticalSection);//进行临界区域
(*freeBlockCounter)++;//设定变量
LeaveCriticalSection(&waveCriticalSection);//退出临界区域
DeleteCriticalSection(&waveCriticalSection);//删除临界区域
注意,您可以定义多个临界区域物件,比如 cs1 和 cs2。例如,如果一个程式有四个执行绪,而前两个执行绪共用一些资料,那么它们可以使用一个临界区域物件,而另外两个执行绪共用一些其他的资料,那么它们可以使用另一个临界区域物件。 您在主执行绪中使用临界区域时应该小心,如果从属执行绪在它自己的临界区域中花费了一段很长的时间,那么它可能会将主执行绪的执行阻碍很长一段时间。
至此播放器就顺利地诞生了,尽管它还比较粗糙,但各项功能还算完备。不过,请注意此播放器只能播放wave文件,不支持其它类型的文件。下面就是完整代码的附件,有需要的就请下载。点击打开下载页面

原创粉丝点击