Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DirectX MIDI - Simultaneously Play Multiple MIDI Files from Memory Using MCI Functions

0.00/5 (No votes)
10 Oct 2020 1  
In this article, you will see the steps required to simultaneously play multiple midi file from memory using MCI functions.
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

(1.1)single_process__single_midi.dll

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!

(1.2)multi_process__each_process_1_midi.dll

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.

(1.3)single_process__multi_midi.dll

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

Image 1

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here