DirectX MIDI - 使用 MCI 函数同时播放内存中的多个 MIDI 文件





3.00/5 (1投票)
在本文中,您将看到使用 MCI 函数从内存同时播放多个 midi 文件所需的步骤。
引言
就像矢量图形文件记录线条的 [begin_coor
, end_coor
, color
和 thickness
] 一样,midi 文件记录波形的 [keyDown_tick
, keyUp_tick
, frequency
和 musical_instrument
]。由于记录的数据较少,因此 midi 应该是压缩比最高的音频数据文件格式,500k 字节至少可以播放 1 小时。换句话说,midi 可以减小多媒体文件的大小。
之后,我们可以从内存中播放多个 midi 文件,然后我们可以将音频数据嵌入到 exe_file
的资源中并像往常一样播放它们。
并且我们可以完善一些罕见的多媒体格式,例如
- (0.4.1) Apple 的 QuickTime,它支持“QuickTime 矢量”和 midi 流
- (0.4.2) Barry Kauler 的 EVE,他希望从 2004 年开始将 mp3(甚至不是 midi)嵌入到他的多媒体格式中
背景
MIDI 文件 | “Musical Instrument Digital Interface”文件 |
mci 函数 | MediaControl Interface (媒体控制接口) |
DirectX | 用于播放 multiMedia 的 API |
Using the Code
(1.1)single_process__single_midi.dll
如果我们通过 mci 函数(mciSendString
或 mciSendCommand
)播放 midi 文件,那么 mci 函数将调用 mmioOpen
、mmioRead
、mmioSeek
和 mmioClose
来访问 midi 文件,而这些 mmio
函数将调用 CreateFileW
(在 win16 中,它们调用 OpenFileEx
)、_lread
、_llseek
和 _lclose
。
因此,如果我们拦截上面的 _lxxx
函数,我们就可以从任何地方播放(我们声明的)midi 文件,例如 exe_file
的资源或内存(如本文所示)。
我们在 single_process__single_midi.dll 中这样做了。所以,在我们声明文件后,我们可以从内存中播放单个 midi 文件。但由于 midiOutOpen
只有一个设备(否则我们会得到 MMSYSERR_ALLOCATED
),所以我们只能播放一个文件。太可惜了!
(1.2)multi_process__each_process_1_midi.dll
在另一个进程中,我们可以同时调用 midiOutOpen
。如果我们拦截所有 child_processes
中的 _lxxx
函数来播放多个 midi 文件,并通过管道在 parent_process
中控制所有 child_processes
,那么就完美了。
我们在 multi_process__each_process_1_midi.dll 中这样做。在 DLL 中,我们拦截 MCI 函数,将用户的字符串或命令转换为我们的 cmd_msg_parm_size structure
(例如 mciSendCommandA(xxx,MCI_PLAY,MCI_FROM,&PlayParms)
,它将被翻译为 cmd=ENUM_CMD_A
、msg=MCI_PLAY
、parm=MCI_FROM
、size=sizeof_MCI_PLAY_PARMS
),将此结构发送到 child_process
,并将结果反馈给用户。
但是,多个 child_processes
?这真的很尴尬。
(1.3)single_process__multi_midi.dll
无论如何,我们已经拦截了 mci 函数,并且已经分析了用户的字符串或命令,为什么不使用 directX 呢?
我们在 single_process__multi_midi
.dll 中这样做。
(1.4)demo.exe
在 demo.exe 中,我们显示了 2 个对话框和一个 3_(radio)button_group
(3 个单选按钮组)。
每个对话框显示
- (1.4.1.1) 一个标题栏,显示
memory_midi
的名称 - (1.4.1.2) 一个滑块(trackbar),显示
memory_midi
的min_time
(当然是==0)、current_time
和max_time
- (1.4.1.3) 3 个[按钮],用于
memory_midi
的播放、暂停、恢复、停止和跳转到
任意位置。 - (1.4.1.4) 一个
editbox
(编辑框),用于描述 (1.4.1.3) 中“跳转到”按钮的“任意位置”。
而 (radio)button_group
(单选按钮组)显示 3 个 DLL 的名称供用户选择。每个 DLL 导出的 API 名称都相同。
- (1.4.2.1)
int declare_file_fail(const char* name, unsigned char* content, unsigned long content_leng)
- (1.4.2.2)
int renege_file_fail(const char* name)
例如,如果我们有一个内存中的 midi 文件“unsigned char midi_data0[0x1234]={...};
”,那么我们可以这样做:
if(declare_file_fail("abc.mid",midi_data0,0x1234))
{error(...);}
else {..
mciSendString("play \\\\mem\\abc.mid",...);
...
mciSendString("pause \\\\mem\\abc.mid",...);
...
mciSendString("resume \\\\mem\\abc.mid",...);
...
mciSendString("seek \\\\mem\\abc.mid to 4567",...);
...
mciSendString("stop \\\\mem\\abc.mid",...);
...
if(renege_file_fail("abc.mid"))
{error(...);}
else {ok(...);}
}
(1.5) 流程图
demo.exe | 父进程 | 子进程 | |||
normal | mciSendStringA("play \\\\mem\\abc.mid",...); | winMm.dll:分析用户命令 | kernel.dll: _lread( hFile, dst, cnt ) | ||
1 个进程,1 个 midi | 我们的 dll:new_fread( hFile, dst, cnt) | ||||
n 个进程,1 个 midi | new_mci_str("play \\\\mem\\abc.mid",...); | 分析用户命令,翻译为 cmd=ENUM_STRING_A, msg=MCI_PLAY, parm=MCI_FROM, size=sizeof MCI_PLAY_PARMS,并将它们发送给子进程。 | 从父进程接收 (cmd, msg, parm, size) 结构,mciSendStringA( "play \\\\mem\\abc.mid",...) | ||
1 个进程,n 个 midi | 我们的 dll: 分析用户命令 | dMusic.dll: pPerformance -> PlaySegment(...) |
(1.6) 拦截
(1.6.1)winMm.dll
这是一项繁琐的工作,用我们自己的函数替换 winMm.dll 的 import_table
中的 _lxxx
函数 API。
原始 API | 更换 |
CreateFileW | new_fopenW |
_lclose | new_fclose |
CloseHandle | new_fclose2 |
_lread | new_fread |
_llseek | new_fseek |
所有这些都在 rplc_lxx.c 中。
我们记录文件指针,从我们(我们声明的)文件内存中读取,并将它们反馈给 mmioRead
或 mmioSeek
。而 winMm.dll 不知道(或不关心)发生了什么。这就是“拦截”的含义。
我们在 intercept_winmm.c\int get5addr_fail(void) 中从 winMm.dll 的 import_table
获取这五个 API 地址。
if(get_module_handle_fail(...,"kernel32.dll",&hkerl))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"CreateFileW",&addr0))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"_lclose",&addr1))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"CloseHandle",&addr2))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"_lread",&addr3))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"_llseek",&addr4))
{error(...);}
else...
我们使用 intercept_winmm.c\int WriteApi5(){WriteProcessMemory(...)},将这些地址写入 winMm.dll 的 import_table
中。
(1.6.2)host (宿主)
这也是一项繁琐的工作,用我们自己的函数替换宿主(demo.exe)的 import_table
中的 API。
原始 API | 更换 |
mciSendStringA | new_mci_str |
mciSendCommandA | new_mci_cmd |
它们位于 single_process__multi_midi.c 或 multi_process__each_process_1_midi.c 中。
我们在 intercept_host.c\int get2addr_fail(void) 中从宿主的 import_table
获取这两个 API 地址。
hInst=GetModuleHandle(NULL);
if(get_proc_addr_fail(...,hkerl,"CreateFileW",&addr0))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"mciSendStringA",&addr1))
{error(...);}
else if(get_proc_addr_fail(...,hkerl,"mciSendCommandA",&addr2))
{error(...);}
else...
我们使用 intercept_host.c\int WriteApi2(){WriteProcessMemory(...)},将这些地址写入宿主的 import_table
中。
和上面一样,我们分析宿主的字符串或命令,将其转换为我们的数据结构,然后调用子进程或调用 directX。而宿主不知道(或不关心)发生了什么。这就是“拦截”的含义。
而“分析宿主的 mciSendString
”部分工作意味着:(来自 rplc_mci.c\int analse_str_fail(...))
if(!strnicmp(str,"open",4))*p_cmd_enum=0;
else if(!strnicmp(str,"play",4))*p_cmd_enum=1;
else if(!strnicmp(str,"stop",4))*p_cmd_enum=2;
else if(!strnicmp(str,"close",5))*p_cmd_enum=3;
...
(1.7) 编译
用户可以打开 vc6 项目(vc6.dsw)来编译上面提到的 3 个 DLL 和 1 个 EXE。
每个 DLL 和 EXE 都有其单独的 c 文件,名称相同。在文件开头,有关于如何将其从 c 编译为 DLL(或 EXE)的注释。
我们在 mci_function.c 中定义了 MIDXX_USE_STRING
,以选择 (1.4.1.3) 中的“play
, pause
, resume
, stop
, and goto
”方法。如果我们定义了它,那么 dialogXX
将使用 mciSendString
。如果我们没有定义它,那么 dialogXX
将使用 mciSendCommand
。原始的 dialog0
使用 mciSendCommand
,而 dialog1
使用 mciSendString
,因此我们可以同时检查 new_mci_str
和 new_mci_cmd
。
(1.8) 杂项
(1.8.1)问:为什么不使用 midi 流函数?它们也可以从内存播放 midi 文件。
答:要使用 midiStreamOpen
、midiStreamProperty
、midiStreamOut
、midiStreamRestart
、midiStreamClose
,我们必须处理 CALLBACK_EVENT(使用 WaitForSingleObject)
,或处理 CALLBACK_WINDOW
(控制台 EXE 怎么办),或处理 CALLBACK_FUNCTION
(switch case MOM_DONE
, case MOM_POSITIONCB
...)。
这些对于本文来说都太复杂了,这里不讨论。
并且根据 (1.1) 的内容,由于 midiStreamOpen
只有一个设备(否则我们会得到 MMSYSERR_ALLOCATED
),所以我们也只能播放一个文件。
但我们仍然可以在 Google 上搜索这些 midi 流函数,这里有一个来自 David 的结果。
(1.8.2)问:为什么不分析 memory_midi_file 中的所有字节,然后以其他方式播放以避免 MMSYSERR_ALLOCATED?
答:FluidSynth 已经这样做了。
DirectX 也这样做了。
(1.8.3) win16 的 thunk (转发)
在 win95 和 win98 中,winMm.dll 中的 mci 函数不处理任何事情,它们被 thunk 到 mmSystem.dll(16 位代码)。所以我们为它们编写了 single16.dll。
在 single16.dll 中,我们在 callBack_0842
(由 mmio
函数调用)中拦截 5 个 API(如上)。
原始 API | 更换 |
OpenFileEx | new_fopenW |
_lclose | new_fclose |
_hRead | new_fread |
_llseek | new_fseek |
并导出 renege_file_fail()
、declare_file_fail()
。
在 win16 中,只有一个 mmSystem.dll,它可以看到所有 win32 进程。因此,根据 (1.2),没有“另一个进程”同时调用 midiOutOpen
。所以只有 single_process__single_midi.dll thunk 到 single16.dll,而 multi_process__each_process_1_midi.dll 不 thunk。如果我们强行在后者这样做,我们只会得到 MMSYSERR_ALLOCATED
。
关注点
悲伤的故事,没意思
2002 年,在日本,SHARP CO. 发布了 eva(extended vector animation,扩展矢量动画)文件格式,版本为 1.73。它可以运行在 Windows 95/98/98SE/Me/NT/2000/XP 上。IE4 的插件、Macintosh 的插件以及 Netscape 的插件都与它一起发布。它的 SDK(acvi.dll、getPath.dll、gif2bmp.dll、jpg2bmp.dll 和 png2bmp.dll)也已发布。甚至还发布了一套给孩子用的 SDK(SHARP 称之为“kidsEva
”)!正如 wikipedia 所说,“[使用 EVA 格式制作的 10 分钟动画] 仅约 500 KB,而[使用 Flash 制作的相同动画则有几 MB]”。当时,它繁荣昌盛,蒸蒸日上。
但它的 video_file
(视频文件)和 audio_file
(音频文件)是分开的。audio_file
(格式为 midi)仅提供指向 video_file
的 hyper_text_link
(超文本链接),并未将其自身嵌入到整个 multiMedia_file
(多媒体文件)中。因此,在 SDK 中,播放器被迫调用 mciSendString()
来播放外部 audio_file
。当然,反过来,Microsoft 也不支持 memory_midi_file
,例如 PlaySound()
只支持 memory_wave_file
。这会导致 SDK 选择一个懒惰的路径。但,这真的很……奇怪!
2009 年,SHARP CO. 首次出现亏损。
2016 年,SHARP CO. 被整合到 HonHai CO.。
历史
- 2020 年 10 月:修复了 win10 64 位的一些 bug。已提交。
- 2007 年 12 月:可以使用 vc6.0 和 bc 4.5 编译。