MIDIリソース再生の裏技

概要:MIDIリソースを一時ファイルに書き出し、MCIで再生

MIDIファイルをメモリに読み込んでから演奏したい場合もあるでしょう。
「処理を高速化したい」「ファイルを公開したくない」「編集したい」……等々。
もちろん方法はあります。
MIDIファイルを解析して、ショートメッセージとロングメッセージを送り続けるのです。
しかし、MIDIファイルはプログラムで扱いやすいようには設計されておらず、解析は困難です。
簡単に実現する為には何としてもMCIに演奏してもらうしかありません。

今回は既存のMIDIファイルをリソース登録したもについて解説します。

■MIDIリソースをMCIに再生させる

MCIが対応しているのはファイルだけです。
リソースはデータなので、このままでは操作する事ができません。
じゃあ、リソースデータを一時ファイルに書き込んじゃおう!というのが今回の解法。
非効率的だし、ファイル隠蔽の効果も薄れますが、
よく使われる方法みたいですし、妥協策として採用するのも悪くありません。

一時ファイルは必要な時に作成して、不要になったら直ちに削除します。

■一時ファイル

一時ファイルって何?って感じですが、
一時ファイルは普通のファイルと何ら変わりがないと思われます。
拡張子は .tmp です。

■MIDIファイルをメモリに読み込みたい事情

最初に列挙した動機ついて考察してみましょう。

○処理を高速化したい

MCIは遅いのか?という疑問が焦点になりますが、
いろいろ工夫されているでしょうし、MIDIファイルは非常にサイズが小さいので
遅れは気にならないと思われます。

○ファイルを公開したくない

リソースにしても知識あるユーザーには効力がありませんが、ファイル暗号化までするのは面倒です。
残念ながら、今回の一時ファイルに書き出す方法では
最低でも実行中はファイルが存在するので、発見されてしまう可能性がありますが、
直接公開するという事態だけは避けられます。

○編集したい

編集するにはやっぱりMIDIファイルを解析するしかありません。

■MIDIファイルをリソースに登録する

「リソースの追加」→「インポート」よりMIDIファイルを指定します。
MIDIファイルはカスタムリソースなので、「リソースの種類」を入力する事を求められます。
任意の名前を付けられますが、今回は「MIDI」とします。



ここで、MIDIファイルは「jazz.mid」を選択しています。
リソースIDは "JAZZ" に変更しました(""必須)。

■一時ファイルを作成する

一時ファイル作成の手順は次のようになります。

1.ディレクトリのフルパス取得
2.ファイルのフルパス作成

一時ファイルには標準ディレクトリが用意されています。
一時ファイル用のディレクトリのフルパスを取得するには GetTempPath 関数を使います。

DWORD GetTempPath(
  DWORD nBufferLength,  // バッファのサイズ
  LPTSTR lpBuffer      // パスを格納するバッファ
);


関数が成功すると lpBuffer に一時ファイル用のディレクトリのフルパスが格納されます。

次に、ファイルのフルパスを作成します。
一時ファイルのファイル名を作成するには GetTempFileName 関数を使います。

UINT GetTempFileName(
  LPCTSTR lpPathName,    // ディレクトリ名
  LPCTSTR lpPrefixString,   // ファイル名の接頭辞
  UINT uUnique,         // 整数
  LPTSTR lpTempFileName  // ファイル名を格納するバッファ
);


lpPathName にはディレクトリのフルパスを指定します。
 "."を指定した場合はカレントディレクトリを指定した事になります。
lpPrefixString にはファイル名の先頭3文字を指定することができます。
 ランダムな文字列が好ましいと思われるので後述の自作関数で作成します。
uUnique には 0 を指定して下さい。
 0 を指定すると一意のファイル名を作成して、ファイルを作成します。
 0 以外を指定すると(一意じゃないかもしれない)ファイル名を作成して、ファイルは作成しません。
  一意性のテストはファイルを作る CreateFile 関数などで行う事になります。
lpTempFileName にはファイルのフルパスが格納されます。
 MAX_PATH 文字以上のバッファを与えて下さい。

ファイル名の4文字目以降は16進文字列です。

関数が成功すると、ファイル名の4文字目以降に使われた数値が返り、失敗すると 0 が返ります。

char dirpath[MAX_PATH],pre[4];
GetTempPath(sizeof(dirpath),dirpath);
randstr(3,pre);
if(!GetTempFileName(dirpath,pre,0,FileName)){
    FileName[0]='\0';
    MessageBox(NULL,"一時ファイル作成に失敗しました",NULL,MB_OK);
    return -1;
}

FileName はファイルの名を格納するバッファのポインタです。
randstr 関数はランダムな文字列を生成する自作関数です。

■ランダムな文字列を生成する

一時ファイルの先頭3文字は小文字だけ、または大文字だけのパターンが多いようです(私の環境では)。
従って、randstr 関数の仕様もそのようにしました。

void randstr(int n,char *rs)
{
    static int random=0;
    if(random==0){
        srand((unsigned)time(NULL));  // 乱数の種を蒔く
        random=1;
    }

    const char sml[]="abcdefghijklmnopqrstuvwxyz";
    const char lrg[]="ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    int i;
    if(rand()%2) for(i=0;i<n;i++) rs[i]=sml[rand()%(sizeof(sml)-1)];
    else for(i=0;i<n;i++) rs[i]=lrg[rand()%(sizeof(lrg)-1)];
    rs[n]='\0';
}

引数 n に作成して欲しい文字数を指定します。
引数 rs は文字列を格納するバッファのアドレスです。n+1バイト以上必要です。

■ファイルを開く

GetTempFileName 関数はファイルを作成した後に閉じるので、
CreateFile 関数で既存のファイルを開く必要があります。
ファイルが見つからなければ、終了します。

HANDLE fh=CreateFile(FileName,GENERIC_WRITE,0,NULL,
    OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(fh==INVALID_HANDLE_VALUE){
    MessageBox(NULL,"書き込み対象のファイルが存在しません",FileName,MB_OK);
    return -2;
}

■リソースのアドレスを取得する

リソースのアドレスを取得する手順は次のようになります。

1.リソースの場所を調べる
2.リソースをロードする
3.リソースをロックする

リソースの場所を調べるには FindResource 関数を使います。

HRSRC FindResource(
  HMODULE hModule,  // モジュールのハンドル
  LPCTSTR lpName,   // リソース名
  LPCTSTR lpType    // リソースタイプ
);


hModule にはリソースが入った実行可能ファイルを持つモジュールのハンドルを指定します。
 自分なら NULL を指定します。
lpName にはリソース名を指定します。
lpType にはリソースタイプを指定します。今回は "MIDI" です。

関数が成功すると、指定したリソースの情報ブロックのハンドルが返ります。
リソースのハンドルを取得するには、このハンドルを LoadResource 関数に渡してください。

HGLOBAL LoadResource(
  HMODULE hModule,  // モジュールのハンドル
  HRSRC hResInfo    // リソースのハンドル
);


hModule にはリソースが入った実行可能ファイルを持つモジュールのハンドルを指定します。
 自分なら NULL を指定します。
hResInfo にはロードしたいリソースのハンドルを指定します。
 FindResource 関数の戻り値を指定して下さい。

関数が成功すると、リソースに関連付けられたデータのハンドルが返ります。
リソースデータへのポインタを取得するには、LockResource 関数に渡して下さい。

LPVOID LockResource(
  HGLOBAL hResData  // リソースのハンドル
);


hResData にはロックしたいリソースを指定します。
 LoadResource 関数の戻り値を指定して下さい。

関数が成功すると、リソースの最初のバイトへのポインタが返ります。

HRSRC hrs=FindResource(NULL,ResourceName,"MIDI");
if(hrs==NULL){
    CloseHandle(fh);
    DeleteFile(FileName);
    FileName[0]='\0';
    MessageBox(NULL,"指定したリソースは存在しません",ResourceName,MB_OK);
    return -3;
}
HGLOBAL hgMidi=LoadResource(NULL,hrs);
LPBYTE lpMidi=(LPBYTE)LockResource(hgMidi);

ResourceName はリソースIDを指すポインタです。

■書き込み&クローズ

リソースのバイト数は SizeofResource 関数で得られます。

DWORD SizeofResource(
  HMODULE hModule,  // リソースモジュールのハンドル
  HRSRC hResInfo    // リソースのハンドル
);


hModule にはリソースが入った実行可能ファイルを持つモジュールのハンドルを指定します。
 自分なら NULL を指定します。
hResInfo にはバイト数を調べたいリソースのハンドルを指定します。
 FindResource 関数の戻り値を指定して下さい。

DWORD dwWrite;
WriteFile(fh,lpMidi,SizeofResource(NULL,hrs),&dwWrite,NULL);

CloseHandle(fh);

以上で、MIDIリソースの内容を書き出した一時ファイルができました。
MCIに一時ファイルの名前を指定すれば演奏する事ができます。

■一時ファイルを削除する

一時ファイルは自動的に削除されません。
むしろ自動的に削除して欲しいところですが、明示的に削除する必要があります。
証拠を残したくなければ、忘れず削除しましょう。

一時ファイルを削除するには普通のファイルと同じく DeleteFile 関数を使います。

ただし、異常終了した場合などで DeleteFile 関数が実行されなければ、ファイルは残ってしまいます。
これはかなり困った問題ですが、有効な解決策を私は知りません。

void DeleteRscFile(char *FileName)
{
    if(FileName[0]!='\0'){
        DeleteFile(FileName);
        FileName[0]='\0';
    }
}

■RscFile.cpp

以上のプログラムをまとめて示します。

GetRscFileName 関数が一時ファイルを作成してリソースデータを書き出す処理を行います。
もちろんMIDIリソース以外にも使えます。

#include<windows.h>
#include<time.h>

// ランダムな文字列を生成する
// int n:生成する文字数
// char *rs:生成した文字列を格納するバッファ( n+1 バイト以上必要)

void randstr(int n,char *rs)
{
    static int random=0;
    if(random==0){
        srand((unsigned)time(NULL));  // 乱数の種を蒔く
        random=1;
    }

    const char sml[]="abcdefghijklmnopqrstuvwxyz";
    const char lrg[]="ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    int i;
    if(rand()%2) for(i=0;i<n;i++) rs[i]=sml[rand()%(sizeof(sml)-1)];
    else for(i=0;i<n;i++) rs[i]=lrg[rand()%(sizeof(lrg)-1)];
    rs[n]='\0';
}

// リソースを一時ファイルに書き出し、そのフルパスを格納する
// LPCTSTR ResourceName:リソース名
// char *FileName:フルパスを格納します( MAX_PATH バイト以上必要)
// 戻り値:0(成功) or 負値(失敗)

int GetRscFileName(LPCTSTR ResourceName,char *FileName)
{
    char dirpath[MAX_PATH],pre[4];
    GetTempPath(sizeof(dirpath),dirpath);
    randstr(3,pre);
    if(!GetTempFileName(dirpath,pre,0,FileName)){
        FileName[0]='\0';
        MessageBox(NULL,"一時ファイル作成に失敗しました",NULL,MB_OK);
        return -1;
    }

    HANDLE fh=CreateFile(FileName,GENERIC_WRITE,0,NULL,
        OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if(fh==INVALID_HANDLE_VALUE){
        MessageBox(NULL,"書き込み対象のファイルが存在しません",FileName,MB_OK);
        return -2;
    }

    HRSRC hrs=FindResource(NULL,ResourceName,"MIDI");
    if(hrs==NULL){
        CloseHandle(fh);
        DeleteFile(FileName);
        FileName[0]='\0';
        MessageBox(NULL,"指定したリソースは存在しません",ResourceName,MB_OK);
        return -3;
    }
    HGLOBAL hgMidi=LoadResource(NULL,hrs);
    LPBYTE lpMidi=(LPBYTE)LockResource(hgMidi);

    DWORD dwWrite;
    WriteFile(fh,lpMidi,SizeofResource(NULL,hrs),&dwWrite,NULL);

    CloseHandle(fh);
    return 0;
}

// ファイルを削除します
// char *FileName:フルパスで指定して下さい

void DeleteRscFile(char *FileName)
{
    if(FileName[0]!='\0'){
        DeleteFile(FileName);
        FileName[0]='\0';
    }
}

■RscFile.h

ヘッダーファイルには以下のように記述しました。
randstr 関数は公開する必要がないので、宣言しません。

// RscFile.cpp

int GetRscFileName(LPCTSTR ResourceName,char *FileName);
void DeleteRscFile(char *FileName);

■MIDIリソースを再生する

関数の呼び出し側は以下のようになります。
演奏はMCIなら何でも良いですが、今回はMCIコマンドを使っています。

ここで、一つ注意しなければならないのはMCI_OPEN_PARMS構造体の lpstrElementName です。
このメンバ変数はアドレスを保存しているのであって、文字列を保存しているわけではありません。
従って、ポインタが指す文字列は静的な領域に配置されていなければなりません。
ファイル名を保存する変数を静的にしているのはその為です。

また、一時ファイルの削除は忘れずに行って下さい。

#include<windows.h>
#include"RscFile.h"

// winmm.lib をリンクする
#pragma comment(lib,"winmm")

LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
    static MCI_OPEN_PARMS mop;
    static char midiFilePath[MAX_PATH];

    switch(uMsg){
        case WM_CREATE:
            GetRscFileName("JAZZ",midiFilePath);
            mop.lpstrDeviceType="Sequencer";      // lpstrElementName はアドレスを保存する
            mop.lpstrElementName=midiFilePath;    // (文字列ではない)
            mciSendCommand(NULL,MCI_OPEN,MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,(DWORD_PTR)&mop);

            mciSendCommand(mop.wDeviceID,MCI_PLAY,0,0);
            return 0;
        case WM_DESTROY:
            mciSendCommand(mop.wDeviceID,MCI_CLOSE,0,0);
            DeleteRscFile((char*)mop.lpstrElementName);    // 忘れないように!
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

★☆ ダウンロード ☆★


戻る / ホーム