Here we will show how to simultaneously play multiple midi files from memory using MCI functions.
Introduction
Like vector graphics file record the [begin_coor
, end_coor
, color
and thickness
] of a line, midi file record the [keyDown_tick
, keyUp_tick
, frequency
and musical_instrument
] of a wave. Because of less record, so midi should be the highest compress_ratio
audio data file format, 500k byte can be played for 1 hour at least. In other words, midi can decrease the size of the multimedia file.
Later, we can play multiple midi files from memory, then we can embed audio data into exe_file
's resource and play them as usual.
And we can consummate some rare multiMedia format, example for:
- (0.4.1) Apple's QuickTime, it supports "QuickTime vector" and midi stream
- (0.4.2) Barry Kauler's EVE, he wishes to embed mp3 (even not midi) into his multiMedia format from 2004
Background
MIDI File | "Musical Instrument Digital Interface" File |
mci functions | MediaControl Interface |
DirectX | API for play multiMedia |
Using the Code
If we play midi file by mci functions (mciSendString
or mciSendCommand
), then the mci functions will call mmioOpen
, mmioRead
, mmioSeek
and mmioClose
to access the midi file, and those mmio
functions will call CreateFileW
(in win16, they call OpenFileEx
), _lread
, _llseek
and _lclose
.
So, if we intercept the _lxxx
functions above, then we can play midi file (which declared by us) from anywhere, example for exe_file
's resource or memory (like this article).
And we do that in single_process__single_midi.dll. So we can play a single midi file from memory, after we declare our file. But because there is only 1 device for midiOutOpen
(otherwise we will get MMSYSERR_ALLOCATED
), so we can only play just 1 file. What a pity!
In another process, we can call midiOutOpen
at the same time. If we intercept the _lxxx
functions in all multiple child_processes
to play multiple midi files, and we control all child_processes
in our parent_process
by pipe, so perfect.
And we do that in multi_process__each_process_1_midi.dll. In the DLL, we intercept MCI functions, translate the user's string or command into our cmd_msg_parm_size structure
(example for mciSendCommandA(xxx,MCI_PLAY,MCI_FROM,&PlayParms)
, it will be translated into cmd=ENUM_CMD_A
, msg=MCI_PLAY
, parm=MCI_FROM
, size=sizeof_MCI_PLAY_PARMS
), send this structure to child_process
, and feed back the result to user.
But, multiple child_processes
? It's really awkward.
Anyway, we had intercepted mci functions, and had analysed the user's string or command, why not use directX?
And we do that in single_process__multi_midi
.dll.
(1.4)demo.exe
In demo.exe, we show 2 dialogs and a 3_(radio)button_group
.
Each dialog shows:
- (1.4.1.1) A titlebar to show the
memory_midi
's name - (1.4.1.2) A trackbar(slide) to show the
memory_midi
's min_time
(of course==0), current_time
and max_time
- (1.4.1.3) 3 [push]button to
play
, pause
, resume
, stop
and goto
anywhere for the memory_midi
. - (1.4.1.4) An
editbox
to describe the "anywhere" for the goto
button in (1.4.1.3).
And the (radio)button_group
shows 3 DLLs' names for user to choose. Each of DLL export APIs are same names:
- (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)
For example, if we have a midi file in memory "unsigned char midi_data0[0x1234]={...};
", then we can do:
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) Flow chart
| demo.exe | parent process | child process | |
normal | mciSendStringA("play \\\\mem\\abc.mid",...); | | winMm.dll: analse user's command | kernel.dll: _lread( hFile, dst, cnt ) |
1 process,1 midi | our dll:new_fread( hFile, dst, cnt) |
n process,1 midi | new_mci_str("play \\\\mem\\abc.mid",...); | analysed the user's command, translate into cmd=ENUM_STRING_A, msg=MCI_PLAY, parm=MCI_FROM, size=sizeof MCI_PLAY_PARMS, and send them to child | receive the (cmd, msg, parm, size) structure from parent, mciSendStringA( "play \\\\mem\\abc.mid",...) |
1 process,n midi | | our dll: analse user's command | dMusic.dll: pPerformance -> PlaySegment(...) |
(1.6)intercept
(1.6.1)winMm.dll:
That's a stuffy working, replace _lxxx
functions API from import_table
of winMm.dll:
org api | replace |
CreateFileW | new_fopenW |
_lclose | new_fclose |
CloseHandle | new_fclose2 |
_lread | new_fread |
_llseek | new_fseek |
All of them are in rplc_lxx.c.
We record the file pointer, read from our file (which declared by us) memory, feed them back to mmioRead
or mmioSeek
. And winMm.dll doesn't know (or doesn't care) what happened. It is just the meaning of "intercept".
We get these five API addresses from import_table
of winMm.dll, in intercept_winmm.c\int get5addr_fail(void).
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...
and we use intercept_winmm.c\int WriteApi5(){WriteProcessMemory(...)}, to write at these five addresses in import_table
of winMm.dll.
(1.6.2)host:
That's a stuffy working too, replace API from import_table
of host (demo.exe):
org api | replace |
mciSendStringA | new_mci_str |
mciSendCommandA | new_mci_cmd |
They are in single_process__multi_midi.c, or multi_process__each_process_1_midi.c.
We get these two API addresses from import_table
of host, in intercept_host.c\int get2addr_fail(void).
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...
and we use intercept_host.c\int WriteApi2(){WriteProcessMemory(...)}, to write at these two addresses in import_table
of host.
Like above, we analyse the host's string or command, we translate them into our data structure, we call child_process or call directX. And the host doesn't know (or doesn't care) what happened. It is just the meaning of "intercept".
And part of the working from "analyse the host's mciSendString
" means: (from 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)compile
User can open vc6 project (vc6.dsw) to compile 3 DLL 1 EXE as above.
Every DLL and EXE has its single c file, with a same name. At the front of the file, there are notes for how to compile it from c to DLL (or EXE).
And we define MIDXX_USE_STRING
in mci_function.c, to choose "play
, pause
, resume
, stop
, and goto
" method in (1.4.1.3). If we define it, then the dialogXX
uses mciSendString
. If we don't define it, then the dialogXX
uses mciSendCommand
. The origin is dialog0
uses mciSendCommand
and dialog1
uses mciSendString
, so we can check both new_mci_str
and new_mci_cmd
.
(1.8)Sundries
(1.8.1)Q: Why not use midi stream functions? They can play midi files from memory too.
A:To use midiStreamOpen
, midiStreamProperty
, midiStreamOut
, midiStreamRestart
, midiStreamClose
, we have to handle CALLBACK_EVENT(use WaitForSingleObject)
, or handle CALLBACK_WINDOW
(how about console EXE), or handle CALLBACK_FUNCTION
(switch case MOM_DONE
, case MOM_POSITIONCB
...).
All of these are too complex for this article, they will not be discussed here.
And according to (1.1), because there is only 1 device for midiStreamOpen
(otherwise, we will get MMSYSERR_ALLOCATED
), so we can only play just 1 file too.
But we can still search for these midi stream functions on Google, and here is a result from David.
(1.8.2)Q: Why not analyse all bytes in memory_midi_file, and play them in another way to avoid MMSYSERR_ALLOCATED?
A: FluidSynth had already done that.
And directX had done that too.
(1.8.3)thunk for win16
In win95 and win98, mci functions in winMm.dll don't handle anything, they are thunk to mmSystem.dll (16 bit code). So we write single16.dll for them.
In single16.dll, we intercept 5 API (like above) in callBack_0842
(which is called by mmio
functions):
org api | replace |
OpenFileEx | new_fopenW |
_lclose | new_fclose |
_hRead | new_fread |
_llseek | new_fseek |
and export renege_file_fail()
, declare_file_fail()
too.
In win16, there is only one mmSystem.dll, and it can see every win32 process. So according to (1.2), no "another process" call midiOutOpen
at the same time. So only single_process__single_midi.dll thunk to single16.dll, and multi_process__each_process_1_midi.dll NOT thunk. If we force to do that in the later, we will only get MMSYSERR_ALLOCATED
.
Points of Interest
Tragical story, not interesting:
At 2002, in Japan, from SHARP CO., the eva (extended vector animation) file format, which version is 1.73, was released. She could be played in Windows 95/98/98SE/Me/NT/2000/XP. PlugIn for IE4, plugIn for Macintosh, and plugIn for Netscape, both had been released with her. The sdk for her (acvi.dll, getPath.dll, gif2bmp.dll, jpg2bmp.dll and png2bmp.dll) were released too. Even a set of sdk for her to kids, was released too (SHARP call it "kidsEva
")! And she had claimed that "A [10_minute animation produced in the EVA format] is only around 500 KB, whereas the [same animation produced in Flash] would be several MB in size" in wikipedia. At that time, she was prosperous, flourishing and thriving.
But her video_file
and audio_file
were separated. The audio_file
(which format was midi) only give a hyper_text_link
into the video_file
, NOT embed itself into the whole multiMedia_file
. So, in SDK, the player was compelled to call mciSendString()
to play the external audio_file
. Of course, conversely, Microsoft didn't support memory_midi_file
, example for PlaySound()
only supported memory_wave_file
. That would cause the SDK to choose a lazy path. But, it was very...strange!
In 2009, SHARP CO. lost money in business first time.
In 2016, SHARP CO. was combined into HonHai CO.
History
- October, 2020: Fixed some bugs in win10 64 bit. Submitted.
- December, 2007: Could be compiled with vc6.0 and bc 4.5