65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2020 年 10 月 10 日

CPOL

8分钟阅读

viewsIcon

7408

downloadIcon

156

在本文中,您将看到使用 MCI 函数从内存同时播放多个 midi 文件所需的步骤。

引言

  • (0.1) 如何从内存播放 midi 文件?
  • (0.2) 如何同时播放多个 midi 文件?
  • (0.3) 如何在单个进程中播放多个 midi 文件?

就像矢量图形文件记录线条的 [begin_coor, end_coor, colorthickness] 一样,midi 文件记录波形的 [keyDown_tick, keyUp_tick, frequencymusical_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 函数(mciSendStringmciSendCommand)播放 midi 文件,那么 mci 函数将调用 mmioOpenmmioReadmmioSeekmmioClose 来访问 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_Amsg=MCI_PLAYparm=MCI_FROMsize=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_midimin_time(当然是==0)、current_timemax_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.dllimport_table 中的 _lxxx 函数 API。

原始 API 更换
CreateFileW new_fopenW
_lclose new_fclose
CloseHandle new_fclose2
_lread new_fread
_llseek new_fseek

所有这些都在 rplc_lxx.c 中。

我们记录文件指针,从我们(我们声明的)文件内存中读取,并将它们反馈给 mmioReadmmioSeek。而 winMm.dll 不知道(或不关心)发生了什么。这就是“拦截”的含义。

我们在 intercept_winmm.c\int get5addr_fail(void) 中从 winMm.dllimport_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.dllimport_table 中。

(1.6.2)host (宿主)

这也是一项繁琐的工作,用我们自己的函数替换宿主(demo.exe)的 import_table 中的 API。

原始 API 更换
mciSendStringA new_mci_str
mciSendCommandA new_mci_cmd

它们位于 single_process__multi_midi.cmulti_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_strnew_mci_cmd

(1.8) 杂项

(1.8.1)问:为什么不使用 midi 流函数?它们也可以从内存播放 midi 文件。

答:要使用 midiStreamOpenmidiStreamPropertymidiStreamOutmidiStreamRestartmidiStreamClose,我们必须处理 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.dllgetPath.dllgif2bmp.dlljpg2bmp.dllpng2bmp.dll)也已发布。甚至还发布了一套给孩子用的 SDK(SHARP 称之为“kidsEva”)!正如 wikipedia 所说,“[使用 EVA 格式制作的 10 分钟动画] 仅约 500 KB,而[使用 Flash 制作的相同动画则有几 MB]”。当时,它繁荣昌盛,蒸蒸日上。

但它的 video_file(视频文件)和 audio_file(音频文件)是分开的。audio_file(格式为 midi)仅提供指向 video_filehyper_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 编译。
© . All rights reserved.