| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 5236 人关注过本帖, 1 人收藏
标题:[原创]发一个程序吧,一个播放音频的解决方案
只看楼主 加入收藏
RockCarry
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
威 望:13
帖 子:662
专家分:58
注 册:2005-8-5
收藏
得分:0 

==================
RME 实现原理分析
==================

RME 全名为 RockCarry Media Engine,是我为解决 Windows 平台下,DOS 程序的音频播放问题而
制作的一个引擎。为什么叫引擎,而不叫 Lib,就是因为它不仅仅是一个 Lib,还包括了许多特别
的东西。

问题的提出。为什么要写 RME 呢?可能个人是一个偏执狂,偏执于在现代化的今天,使用古老的编
译器、研究古老的技术,偏执于对计算机世界本质的探索和追求。所以我目前研究得最深入的还是
DOS 操作系统、Turbo C 编译器。

要解决的问题是什么呢?大家都知道 windows 操作系统是可以兼容运行 DOS 程序,但是在
WindowsXP 系统中,对 DOS 程序的兼容性做得比较差,特别是对音频部分的兼容性做得不好,我做
过测试 WindowsXP 的虚拟 DOS 只能支持到 SB2.01 标准,并且效率也非常差,使用 DMA 方式播放
音频,CPU 也经常用到 50% 以上,可见 WindowsXP 对声卡的支持是通过软件模拟实现的。

许多情况下,我们希望我们的 DOS 程序能够在 XP 上运行,并且能够播放音频。由于 XP 对 DOS
程序的兼容性问题,使得依靠传统的 DOS 声卡编程技术也难以达到以上目的(其实还是可以实现的
)。另一方面,由于我们希望在 WindowsXP 上运行 DOS 程序,因此就可以依靠强大的 Windows 平

台所提供的功能,采用非传统的 DOS 编程技术来达到我们的目的,这个技术的核心就是如何让 DOS
程序与 Windows 通信。如果 DOS 程序能够与 Windows 进行通信,那么就可以有办法让 DOS 程序
调用 WinAPI (当然,是间接的),这样所有 DOS 编程技术无法解决的技术难题,都可以交给
Windows 去解决,而前面提到的音频播放的问题也就迎刃而解了。RME 就是利用这样的技术现实的。

DOS 能不能调用 WinAPI ?答案是肯定的,当然不能。直接的 WinAPI 调用在 DOS 程序中是不可能
做到的,但是我们能够想到间接的办法。这个办法就是 Turbo C 提供的 system 函数,该函数的原
型如下:
int _Cdecl system(const char *command);
参数 command 是一个命令字符串,调用该函数,其结果就等于在命令行中敲入了 command 字符串
所给出的命令。

在 Windows 下同样也有命令行的概念,并且在命令行下敲入一个可执行程序的名字,就是执行这个
程序。那么在 DOS 下,Turbo C 所提供的 system 函数是否真正有这样神奇的功能呢?我们可以写
一个测试程序,如下:

+------------------------------------------------------------+
#include <stdlib.h>
#include <process.h>

void main()
{
system("notepad");
getch();
}
+------------------------------------------------------------+

利用 TC2.0 编译并在 XP 下运行以上程序,我们可以发现弹出了一个记事本。这也证明了,一个
DOS 程序的确可以调用 Windows 的程序。并且,经过我的测试,system 函数在 XP 的功能与在命
令行中敲入命令基本上是完全等效的,也就是说,你能在 Windows 的命令行下敲入那些命令来实现
那些功能,你就能通过 system 函数来实现全部相同的功能。

这样就为我们解决问题找到了突破口。最简单的想法就是,写一个 windows 的命令行程序,使其能
够播放音频文件,这在 Windows 程序中是很容易实现的,不管是使用 MCI 还是 DirectX,都是比
较容易实现的,其实最简单的是 PlaySound 函数,直接调用就可以,例如:

+------------------------------------------------------------+
// RME V0.1
#include <windows.h>

void main(int argc, char *argv[])
{
if (argc < 2) return;
PlaySound(argv[1], NULL, SND_FILENAME | SND_SYNC);
}
+------------------------------------------------------------+

使用 VC 编译以上程序,将程序命名为 RME01.exe,就可以使用了,如下:

+------------------------------------------------------------+
#include <stdlib.h>
#include <process.h>

void main()
{
system("RME01 test.wav"); /*播放test.wav文件*/
getch();
}
+------------------------------------------------------------+

以上就是最简单的实现,但是这样的实现还存在许多问题:
1. 每播放一个文件就要开启一个 RME01 的进程
2. 无法进行播放进度等控制
3. 只有播放完成以后,控制权才会重新交给 DOS 程序
4. 效率低,每一个功能就要写一个 Windows 程序
5. 不便于使用和管理
6. 更多...

因此,我们需要采用更加先进的架构,一下使我的 RME 中采用的方法:
1. 实现一个名为 RME_Server 的 Windows 程序,作为服务程序
2. 实现一个名为 RME_CMD 的 Windows 程序,作为命令转发程序
3. 实现一个名为 RME_Lib 的 Turbo C 函数,用于与 RME_CMD 通信

DOS 程序与 RME_CMD 通信的方法就是通过 system 函数,如下:
+------------------------------------------------------------+
system("RME_CMD cmdname cmdarg");
+------------------------------------------------------------+

在这里 RME_CMD 只起到了一个命令转发的作用,他并不直接相应 DOS 程序发出的命令,而是由
RME_Server 程序来真正的相应命令。这样做的好处在于,system 函数执行后,DOS 程序将一直处
于等待状态,直到所运行的命令结束。这就需要 system 函数做调用的程序能够非常快速的执行,
否则将会影响整个系统地执行效率。RME_CMD 作为一个命令转发程序,能够快速的执行命令转发的
任务,他只负责命令转发,而命令的执行交给 RME_Server 完成,在命令转发完以后,RME_CMD 立
即运行结束,以保证系统效率。

DOS 程序与 RME_CMD 的通信就是通过 system 函数实现,比较容易理解。而 RME_CMD 与
RME_Server 的通信,则需要通过 Windows 的进程间通信技术来实现。做过 Windows 服务程序的人
对此会比较了解,Windows 平台提供了强大的多线程处理能力,在 RME 中主要采用了事件(Event
)和共享内存的方法进行进程间的通信与同步。主要用到的 API 如下:

+-----------------------------------------------------------------------------+
BOOL CreateProcess(
LPCTSTR lpApplicationName, // name of executable module
LPTSTR lpCommandLine, // command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
BOOL bInheritHandles, // handle inheritance option
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // new environment block
LPCTSTR lpCurrentDirectory, // current directory name
LPSTARTUPINFO lpStartupInfo, // startup information
LPPROCESS_INFORMATION lpProcessInformation // process information
);

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // SD
BOOL bManualReset, // reset type
BOOL bInitialState, // initial state
LPCTSTR lpName // object name
);

HANDLE CreateFileMapping(
HANDLE hFile, // handle to file
LPSECURITY_ATTRIBUTES lpAttributes, // security
DWORD flProtect, // protection
DWORD dwMaximumSizeHigh, // high-order DWORD of size
DWORD dwMaximumSizeLow, // low-order DWORD of size
LPCTSTR lpName // object name
);

LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // handle to file-mapping object
DWORD dwDesiredAccess, // access mode
DWORD dwFileOffsetHigh, // high-order DWORD of offset
DWORD dwFileOffsetLow, // low-order DWORD of offset
SIZE_T dwNumberOfBytesToMap // number of bytes to map
);

BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress // starting address
);

BOOL SetEvent(
HANDLE hEvent // handle to event
);

DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval
);

BOOL CloseHandle(
HANDLE hObject // handle to object
);
+-----------------------------------------------------------------------------+

有关这些 API 的具体含义和使用方法请参考 MSDN 和 Windows 多线程编程的相关文档,本文不再
介绍。

重点讲一下 RME 的通信和服务流程。
命令的格式:
+------------------------------------------------------------+
RME_CMD cmd arg
+------------------------------------------------------------+
其中:cmd 为命令代号为数字
arg 为命令参数

RME 中所有的命令定义如下:
+------------------------------------------------------------+
#define PBUF_SIZE 1024 // 参数缓冲区大小

#define RME_INIT 0 // RME初始化命令
#define RME_CLOSE 1 // RME关闭命令

#define RME_PLAYWAVE 10 // 播放 WAVE 文件
#define RME_STOPWAVE 11 // 停止播放 WAVE
#define RME_PAUSEWAVE 12 // 暂停播放 WAVE

#define RME_PLAYMIDI 20 // 播放 MIDI 文件
#define RME_STOPMIDI 21 // 停止播放 MIDI
#define RME_PAUSEMIDI 22 // 暂停播放 MIDI
+------------------------------------------------------------+

由于当前的 RME 只是一个演示版本,因此并没有实现更多功能,连 MIDI 的重复播放都没有作,因
此有点不实用。

在 RME 中并不是所有的命令都有参数,例如初始化命令:
+------------------------------------------------------------+
RME_CMD 0
+------------------------------------------------------------+
关闭命令也是如此。

RME 命令的使用流程,也可以叫做 RME 协议:
1. Send RME_INIT commond to RME_Server
RME_CMD 0
This command will start and init the RME_Server.

2. Send media playing control command to RME_Server
RME_CMD 10 test.wav
RME_CMD 20 test.mid
...
After these command send to RME_Server, it will response to these command, and act the cmd you want.

3. Send RME_CLOSE command to RME_Server
RME_CMD 1
This command will stop and close the RME_Server.

RME_CMD 程序可以理解为一个命令行程序,他只是简单的处理命令行的输入,而前面所讲的各种命
令都是通过命令行参数传递给 RME_CMD 程序的。

对于 RME_INIT 命令,RME_CMD 将创建 RME_Server 进程,以开启 RME_Server 服务。代码如下:
+------------------------------------------------------------+
if (cmd==RME_INIT)
{
PROCESS_INFORMATION pinfo = {0};
STARTUPINFO sinfo = {0};
CreateProcess("RME_Server.exe", NULL, NULL, NULL, NULL,
CREATE_NEW_CONSOLE, NULL, NULL, &sinfo, &pinfo);
return TRUE;
}
+------------------------------------------------------------+

RME_Server 程序内部有特别的处理,以保证 RME_Server 程序在 Windows 中只会运行一个实例,
这是通过一个内核对象进行判断的,代码如下:
+------------------------------------------------------------------+
// 使程序只运行一个实例
hRqtEvent = CreateEvent(NULL, FALSE, FALSE, "RME_RQT_Event");
if (GetLastError()==ERROR_ALREADY_EXISTS)
{
CloseHandle(hRqtEvent);
return;
}
+------------------------------------------------------------------+

对于其他命令,都是通过事件和共享内存向 RME_Server 发送。RME_Server 将执行一个 while 循
环,在这个 while 循环中等待命令事件的产生,如下:
+------------------------------------------------------------------+
// 进入Server循环
while(!bExit)
{
WaitForSingleObject(hRqtEvent, INFINITE); // 等待客户请求
// todo: get and handle cmd
// ...
SetEvent(hAckEvent); // 发送应答事件
}
+------------------------------------------------------------------+

在这里使用到了两个有名的事件对象一个是 RME_RQT_Event 用于 RMD_CMD 发出命令请求,一个是
RME_ACK_Event,用于 RME_Server 对 RME_CMD 进行应答。

在发送命令时 RME_CMD 先将命令代码和参数打包到一个 RME_CMD 的结构体变量中,如下:
+------------------------------------------------------------------+
typedef struct
{
int cmd; // 请求命令
int rval; // 返回值
BYTE pbuf[PBUF_SIZE]; // 参数缓冲区
}RME_CMD;
+------------------------------------------------------------------+

然后将这个命令的结构体变量复制到名为 RME_CMD 的共享内存中。之后设置 RME_RQT_Event 事件,
向 RME_Server 发出命令请求。之后等待 RME_Server 的响应,也就是等待 RME_ACK_Event。

RME_Server 在等到 RME_RQT_Event 事件后,会从共享内存取得命令的代码和命令参数,然后对不
同的命令进行处理,处理完后会设置 RME_ACK_Event 事件给 RME_CMD 一个应答。RME_Server 的音
频播放的实现,也就是最简单的采用了 PlaySound 和 MCI。

RME_CMD 在得到 RME_Server 的应答以后,关闭所有句柄、释放资源,然后结束程序。
经典的三次握手,一次命令的发送、接收、处理和应答的流程就是如此简单了。

对于 RME_CLOSE 命令,当 RME_Server 收到该命令后,将推出 while 循环,然后跑到进程的终点,
最终结束服务进程的运行。

在 Client 这一端,RME_CMD 是命令的转发者,而 system 函数保证了 DOS 程序与 RME_CMD 的通
信。另外还需要实现一个 RMELib 的 Turbo C 函数库,以方便 Turbo C 用户使用。由于 RMELib 只
需要简单的调用 system 函数,原理非常简单,因此不再介绍。

整个系统地实现都是相对简单,而且比较合理高效,实际的运行效果也非常理想,绝对可以满足一
般的简单的 DOS 游戏开发的需求。效率的瓶颈就在于 RME 系统三中个模块之间的通信,RME_CMD 与
RME_Server 之间的通信还算比较快,而 system 函数则成为了最大的系统瓶颈。当然,这样的系统
设计,是绝对不可能满足高性能游戏的需求的,这只是在特定平台下的解决办法。如果要开发高性
能的游戏,就选择高性能的平台吧,如流行的 DirectX,这就不在本文的讨论范围。

另外,RME 目前仅仅是一个 Demo,旨在给出一种方法和思路而已。大家可以根据这个思路,设计出
更加实用、更加通用的系统。

RockCarry
2007-3-31







[此贴子已经被作者于2007-6-1 16:58:42编辑过]

2007-05-30 19:51
RockCarry
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
威 望:13
帖 子:662
专家分:58
注 册:2005-8-5
收藏
得分:0 

今天终于完成了
一直都想写,可是现在下班后就什么都不想做,连显示器都不想看。
今天努力了下,把他完成了
还有很多技术文章,都想写出来,可惜啊,可能是自己太累了吧
大家帮忙找找错别字吧。

[此贴子已经被作者于2007-5-30 20:49:57编辑过]

2007-05-30 20:48
一笔苍穹
Rank: 1
等 级:新手上路
帖 子:640
专家分:0
注 册:2006-5-25
收藏
得分:0 
以上就是最简单的实现,但是这样的实现还存在许多问题:
1. 没播放一个文件就要开启一个 RME01 的进程
“没”->“每”

不错,支持!我现在也在做一个类似的C/S程序,原理与这个相似,而且它支持更多的功能,比如图形处理等等。
2007-05-31 08:58
RockCarry
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
威 望:13
帖 子:662
专家分:58
注 册:2005-8-5
收藏
得分:0 
最关键的是要搞懂原理,看到本质。
网上许多开源的代码写的并不是最好的,但是只要弄清楚了原理,就可以自己去实现。
2007-06-02 09:30
yang667455
Rank: 1
等 级:新手上路
帖 子:17
专家分:0
注 册:2007-5-13
收藏
得分:0 
你们真历害呢~可是我有个疑问:
我觉得DOS程序在原理学习和技术研究上还是很有用的.可是在实用方面就大可不必了.毕竟大部分人都用WINDOWS.
既然这样,为什么还要在DOS下折腾呢?
如果要作个播放器,用VC可以方便作出来吧?

不过楼主说得好,
这个也是一个解决问题的办法,更加重要的是给出一个方法和思维方式。
2007-06-07 16:10
RockCarry
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
威 望:13
帖 子:662
专家分:58
注 册:2005-8-5
收藏
得分:0 

编程的乐趣就在于优美的实现,把不可能优雅的变成可能。DOS 的确很落后了,所以有很多的不可能,当然把这些不可能便成为可能的时候,会是无比的激动和兴奋,而自己对编程、对平台、对整个计算机系统的理解也会更加的深入。
Windows 确实太强大了,强大到不用写代码就可以实现许多功能,比如 MFC。但是,少了自己的努力,一切都变得很无趣,你不觉得吗。我当时学 MFC 时真的不知道如何下手,因为代码都给你做好了,你要做的只是添加和修改。

软件开发的实质,就是深入的理解平台,最大限度的利用平台已经提供的功能,去实现自己需要而平台却没有提供的功能。编程的乐趣和所带来的价值,都在于实现上,实现是一个劳动的过程,因此是一个创造价值的过程。如果一个程序很容易实现,那么就少了乐趣,也少了价值。就如同 HelloWorld 程序,谁都会实现,可是谈不上乐趣,更加谈不上价值。

为什么沉迷于 DOS,就是因为 DOS 是一个开放的平台,非常适合于学习一些底层的知识,并且容易入门,相关的参考资料也比较多。当然,再任何一个平台上,都有值得理解和学习的东西,也有需要去实现的东西。实现的过程,其实也是一个平台搭建的过程,平台就是这样一层一层的实现,一层一层的搭建起来的。个人认为,做的层的东西,是很难的,然后却是很有价值的。

如果要问为什么,我的答案就是,我们需要站在巨人的肩膀上,但我们不愿意简单的站在别人的平台上。理解技术的本质,尝试重新实现,夸张的说,自己做出更好的 Windows 吧。

[此贴子已经被作者于2007-6-7 17:51:22编辑过]

2007-06-07 17:25
RockCarry
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
威 望:13
帖 子:662
专家分:58
注 册:2005-8-5
收藏
得分:0 
实用性是一方面,学习是另一方面。
也许实用性达不到要求,但是不会白白努力的,可以学习很多东西。
就算 RME 最终不实用,但是我能把 RME 做到这个程度,对 Windows 的多线程编程技术也已经有了较为深入的理解。
如果我没有这个兴趣,没有这样的执著,恐怕到现在也就是一事无成。
关键要看自己的兴趣了。
2007-06-07 17:56
yang667455
Rank: 1
等 级:新手上路
帖 子:17
专家分:0
注 册:2007-5-13
收藏
得分:0 
RockCarry不仅技术好,心态也好呢.你的观点我同意了.向你学习~以后多指点我这个小初哥哦.

2007-06-07 19:01
anlogo
Rank: 2
等 级:论坛游民
威 望:1
帖 子:293
专家分:20
注 册:2007-7-20
收藏
得分:0 
这么好的贴也不顶,要什么才顶,顶起~~
2007-07-23 16:08
dedicator
Rank: 1
等 级:新手上路
帖 子:1
专家分:0
注 册:2006-8-20
收藏
得分:0 
回复:(RockCarry)[原创]发一个程序吧,一个播放音频...
说到心坎上了。。顶!
2007-08-05 13:24
快速回复:[原创]发一个程序吧,一个播放音频的解决方案
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.023355 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved