はじめに
マルチメディアファイルの制御は、たとえ未圧縮のデータ形式でも複雑なものです。音声ファイルやビットマップファイルを純粋な入出力APIだけを使って生成したり、データを解析しようと考えた場合は、音声やイメージに関する物理的な知識が必要になります。さらに、マルチメディアデータはすべてがバイナリで表現されるため、ファイル形式の仕様を熟知した上で、1バイト単位の複雑な入出力コードを書かなければなりません。
幸い、多くの環境でこうした問題を解決するためのAPIが提供されています。例えばJavaの場合はJava Media Framework APIとしてメディアデータの制御用ライブラリが提供されています。Win32 APIやDirectXでも、同様にマルチメディアデータを制御するための様々な関数が提供されているため、これらを学習することでマルチメディアアプリケーションを開発することができます。
Win32 APIでマルチメディアを扱う代表的な機能はMCI(Media Control Interface) でしょう。MCIを使えば、Windowsが対応している音声データや動画を自由に再生、停止、位置の移動などを行うことができ、例えばマルチメディアプレイヤーを開発することも可能です。しかし、こうした高水準APIは、再生や操作を専門としているため、マルチメディアデータの特定の部位の抽出や編集を行うには、より複雑な低水準APIを使わなければなりません。
今回はVideo for Windows APIを使って、AVI動画ファイルから任意のフレームをビットマップとして抽出し、BMPファイルとして保存するアプリケーションを開発します。
対象読者
本稿は、C言語の基本的な文法を理解し、Windows32 APIでWindowsクライアントアプリケーションの開発経験がある方を対象としています。ハンドルの概念、ウィンドウプロシージャ、メッセージの処理など、基本的なGUIプログラミングの仕組みについての解説は割愛します。
また、AVI動画ファイルのフレームからBMPファイルを生成します。ビットマップの仕組み、およびデバイス独立ビットマップに関して知識があると、より本稿のプログラムを読み取ることができます。
Video for Windowsライブラリの初期化
Video for Windows APIを利用するには、まず開発環境に対してインポートライブラリ「vfw32.lib」をインポートしてください。インポートの方法は、利用しているコンパイラ、または総合開発環境のドキュメントを参照してください。代表的な開発環境であるVisual Studioの場合は、プロジェクトのプロパティから設定します。
プロジェクトを右クリックして[プロパティ]を選択し、ダイアログの左側のツリーから[構成プロパティ]→[リンカ]→[コマンドライン]を展開します。ここにリンカに設定するオプションを追加できるので[追加のオプション]に「vfw32.lib」を指定します(日本語名はすべてVisual Studio 2003のもの)。これで、「vfw32.lib」をインポートすることができます。
プログラムでは、AVI関連の関数を使うソースコードに「vfw.h」ヘッダファイルをインクルードさせる必要があります。これで、基本的な準備は終わります。
Video for Windows APIを利用するには、まずAVIFileInit()
関数を呼び出してAVIFileライブラリを初期化しなければなりません。
STDAPI_(VOID) AVIFileInit(VOID);
他のAVIから始まる関数を呼び出しは、必ずAVIFileInit()
関数よりも後で行わなければなりません。この関数はライブラリの参照カウントをインクリメントします。AVIFileライブラリが不要になればAVIFileExit()
関数で参照カウントをデクリメントすることができます。
STDAPI_(VOID) AVIFileExit(VOID);
アプリケーション全体でAVI関連関数を使うような場合は、WinMain()
関数の最初でAVIFIleInit()
を呼び出し、メッセージループを抜け出した後のreturn
文の直前で、AVIFIleExit()
を呼び出してリソースを解放すればよいでしょう。
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow ) { WNDCLASS wc; MSG msg; //AVIライブラリの初期化 AVIFileInit(); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WindowProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL , IDI_APPLICATION); wc.hCursor = LoadCursor(NULL , IDC_ARROW); wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszMenuName = IDR_MENU1; wc.lpszClassName = APP_NAME; if (!RegisterClass(&wc)) return 0; if (CreateWindow( APP_NAME , APP_NAME , WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_HSCROLL, CW_USEDEFAULT , CW_USEDEFAULT, CW_USEDEFAULT , CW_USEDEFAULT, NULL , NULL , hInstance , NULL ) == NULL) return 0; while(GetMessage(&msg , NULL , 0 , 0) > 0) { DispatchMessage(&msg); } //AVIライブラリを解放する AVIFileExit(); return msg.wParam; }
このコードは、本稿サンプルプログラムのWinMain()
関数です。最初にAVIFIleInit()
関数でライブラリを初期化し、return
文の直前でAVIFileExit()
を呼び出しています。
AVIファイルを開く
AVIファイルを操作するには、何よりもまずAVIファイルを開く必要があります。AVIファイルを開くにはAVIFileOpen()
関数を使います。この関数で開くAVIファイルインタフェースはファイル入出力で利用するハンドルではなく、AVIFileライブラリでファイルを操作するために使う専用のハンドルです。
STDAPI AVIFileOpen(PAVIFILE * ppfile, LPCTSTR szFile, UINT mode, CLSID * pclsidHandler);
AVIファイルのハンドルはPAVIFILE
型として扱われます。ppfile
には、開いたAVIファイルのハンドルを受けるPAVIFILE
へのポインタを指定します。szFile
には開くAVIファイルのファイル名が格納されているNULL
で終わる文字列を指定します。
mode
にはファイルを開くときに使うアクセスモードを指定します。ここには、次のいずれかの定数を指定します。今回の用途はファイルからビットマップデータを抽出することなのでOF_READ
で開きます。
定数 | 意味 |
OF_CREATE | 新しいファイルを作成します。 |
OF_SHARE_DENY_NONE | ファイルを非排他的に開きます。他のプロセスは、このファイルを読み取り専用または書き込み専用で開くことができます。 |
OF_SHARE_DENY_READ | ファイルを非排他的に開きます。他のプロセスは、このファイルを書き込み専用で開くことができます。 |
OF_SHARE_DENY_WRITE | ファイルを非排他的に開きます。他のプロセスは、そのファイルを読み取り専用で開くことができます。 |
OF_SHARE_EXCLUSIVE | ファイルを開き、そのファイルに対する他のプロセスからのアクセスを禁止します。 |
OF_READ | ファイルを読み取り用に開きます。 |
OF_READWRITE | ファイルを読み取りおよび書き込み用に開きます。 |
OF_WRITE | ファイルを書き込み用に開きます。 |
pclsidHandler
には、利用するハンドラの識別子へのポインタを指定します。CLSID
はコンポーネントを識別するための16バイトからなる数値情報ですが、通常はNULL
を指定します。ここにNULL
が指定された場合は、システムがファイルの拡張子やRIFFタイプに基づいて、レジストリから適切なハンドラを選択します。ファイル拡張子やRIFFタイプに依存せずに固定のハンドラを使いたい場合には、NULL
以外の適切な値を指定します。
関数が成功すれば0が、失敗すれば0以外のエラー値が返ります。
開いたAVIファイルを解放するにはAVIFIleRelease()
関数を使います。AVIファイルのハンドルが不要になった場合は、この関数を呼び出して解放してください。
STDAPI_(ULONG) AVIFileRelease(PAVIFILE pfile);
pfile
には開いている有効なAVIファイルのハンドルを指定します。
AVIファイルストリーム
AVIファイルを無事に開くことができれば、PAVIFILE
ハンドルを取得することができます。AVIFileから始まる関数を使ってAVIファイルの情報を取得したり、AVIファイルを操作するにはPAVIFILE
型のハンドルが必要になります。
AVIファイルにデータを書き込んだり、AVIファイルからデータを取得するにはストリームを開く必要があります。AVIファイルのストリームを開くにはAVIFileGetStream()
関数を呼び出します。
STDAPI AVIFileGetStream(PAVIFILE pfile, PAVISTREAM * ppavi, DWORD fccType, LONG lParam);
pfile
には有効なAVIファイルのハンドルを、ppavi
にはAVIファイルから取得するストリームハンドルへのポインタを指定します。関数が成功すればppavi
が参照している変数に、適切なストリームハンドルが格納されます。fccType
には開くストリームの種類を指定します。通常のAVIファイルには音声と映像ストリームがそれぞれ格納されています。fccType
には以下のいずれかの定数を指定します。
定数 | ストリームの種類 |
StreamtypeAUDIO | オーディオストリーム |
StreamtypeMIDI | MIDIストリーム |
StreamtypeTEXT | テキストストリーム |
StreamtypeVIDEO | ビデオストリーム |
ビデオストリームからフレームを取得してビットマップファイルに保存することが今回の目的なので、この場はstreamtypeVIDEO
を指定します。開いているストリームハンドルが不要になればAVIStreamRelease()
関数で解放してください。
STDAPI_(LONG) AVIStreamRelease(PAVISTREAM pavi);
pavi
には有効なAVIストリームハンドルを指定します。
フレーム解凍オブジェクト
AVIファイルを開き、ビデオストリームのハンドルを取得することができれば、いよいよフレームの解凍処理に入ります。AVIフレームは単純なビットマップの配列ではなく、圧縮されているため、まずは解凍オブジェクトを取得しなければなりません。解凍オブジェクトを取得してフレームの解凍を準備するにはAVIStreamGetFrameOpen()
関数を使います。
STDAPI_(PGETFRAME) AVIStreamGetFrameOpen(PAVISTREAM pavi, LPBITMAPINFOHEADER lpbiWanted);
pavi
には有効なAVIビデオストリームのハンドルを指定します。lpbiWanted
には回答結果となるビデオフォーマットを表すBITMAPINFOHEADER
構造体へのポインタを指定します。通常はデフォルトを表すNULL
か、システムで最適な表示フォーマットにデコードすることを表すAVIGETFRAMEF_BESTDISPLAYFMT
を指定します。色数や圧縮形式など、取得するビットマップのフォーマットを指定したい場合は、適切なBITMAPINFOHEADER
構造体へのポインタを指定します。
関数が成功すればPGETFRAME
型の解凍オブジェクトを取得することができます。指定されたフォーマットに解凍できる解凍ツールが発見されなかった場合は、関数が失敗してNULL
が返ります。
AVIフレームの解凍オブジェクトを取得することができれば、AVIStreamGetFrame()
関数からデバイス独立ビットマップ(DIB)を取得することができます。
STDAPI_(LPVOID) AVIStreamGetFrame(PGETFRAME pgf, LONG lPos);
pgf
には、AVIStreamGetFrameOpen()
関数が返した適切なフレーム解凍オブジェクトを指定します。lPos
には、取得するストリーム内のフレーム番号を指定します。この番号は0からストリームの長さまで指定することができます。関数が成功すれば、フレームデータへのポインタを返します。
フレームデータは、パックDIBの先頭へのポインタとして返されるので、BITMAPINFOHEADER
構造体へのポインタとして扱うことができます。ピクセルデータがBITMAPINFOHEADER
構造体に続いて保存されているため、この関数の戻り値から画像をウィンドウに表示させたり、あるいはビットマップファイルとして保存することができます。ただし、AVIStreamGetFrame()
が返したフレームは解凍オブジェクトが解放されるか、またはAVIStreamGetFrame()
関数が次に呼び出されるまでの間のみ有効です。
フレームが何番まで指定できるかどうかは、AVIファイルのビデオストリームの長さに依存します。ストリームの長さを取得するにはAVIStreamLength()
関数を使います。
STDAPI_(LONG) AVIStreamLength(PAVISTREAM pavi);
pavi
にはAVIストリームハンドルを指定します。開いたビデオストリームを指定すれば、AVIStreamGetFrame()
関数のlPos
で指定できる最大値を知ることができます。
AVIStreamGetFrameOpen()
関数で開いた解凍オブジェクトが不要になればAVIStreamGetFrameClose()
関数で解放してください。
STDAPI AVIStreamGetFrameClose(PGETFRAME pget);
pget
には、有効なPGETFRAME
を指定します。
アプリケーション解説
今回開発したサンプルアプリケーションでは、メニューの[AVIを開く]を選択して、表示されたファイル選択ダイアログからAVIファイルを選択すると、ウィンドウ上に選択したAVIファイルの任意のフレームを表示します。フレームは、ウィンドウ下部の水平スクロールバーで移動することができます。表示しているフレームをビットマップイメージとしてディスクに保存するには、「ビットマップで保存」メニューを選択してくださ。
このアプリケーションでは、[AVIを開く]でAVIファイルを選択した際に、AVIファイルを開いてハンドルを初期化するInitAVIResource()
関数と、現在開いているAVI関連リソースを解放するRemoveAVIResource()
関数に注目してください。
//AVI ファイルオブジェクト PAVIFILE aviFile = NULL; //AVI ファイルのストリームオブジェクト PAVISTREAM aviStream = NULL; //AVI ファイルからフレームを解凍するオブジェクト PGETFRAME aviFrame = NULL; //現在のカレントフレーム BITMAPINFOHEADER * bmpInfoHeader = NULL; //AVI ファイルを開いて必要な初期化処理を行う BOOL InitAVIResource(PCTSTR fileName) { //AVIFile を開く if (AVIFileOpen(&aviFile , fileName , OF_READ , NULL)) { return FALSE; } //AVI ストリームを取得する if (AVIFileGetStream(aviFile , &aviStream , streamtypeVIDEO , 0)) { AVIFileRelease(aviFile); aviFile = NULL; return FALSE; } //フレームを解凍するためのオブジェクトを取得 aviFrame = AVIStreamGetFrameOpen(aviStream , AVIGETFRAMEF_BESTDISPLAYFMT); if (aviFrame == NULL) { AVIStreamRelease(aviStream); AVIFileRelease(aviFile); aviStream = NULL; aviFile = NULL; return FALSE; } bmpInfoHeader = (BITMAPINFOHEADER *)AVIStreamGetFrame(aviFrame , 0); return TRUE; } //AVI ファイルに関連するすべてのリソースを解放する void RemoveAVIResource(){ //カレントフレームを NULL に設定して描画されないようにする bmpInfoHeader = NULL; //各リソースを解放 if (aviFrame) AVIStreamGetFrameClose(aviFrame); if (aviStream) AVIStreamRelease(aviStream); if (aviFile) AVIFileRelease(aviFile); aviFrame = NULL; aviStream = NULL; aviFile = NULL; }
InitAVIResource()
関数では、引数から受け取ったファイル名でAVIファイルを開きます。同時にストリームを開き、解凍オブジェクトを取得します。これらの一連の流れで失敗が無ければ、グローバル変数にAVIファイルのハンドル、AVIストリームのハンドル、フレーム解凍オブジェクトを保存し、フレーム0番のイメージをDIBで取得します。
現在選択されているカレントフレームのイメージは、常にグローバル変数のbmpInfoHeader
に保存されるものとし、選択されていない場合はNULL
とします。ウィンドウプロシージャでは、AVIファイル制御にはほとんど関与せず、単純にbmpInfoHeader
をイメージとして扱っているだけです。WM_PAINT
メッセージではbmpInfoHeader
を使って、SetDIBitsToDevice()
関数からウィンドウにイメージを描画しています。
以下はウィンドウプロシージャのWM_PAINT
メッセージの処理です。
case WM_PAINT: hdc = BeginPaint(hWnd , &ps); //カレントフレームが NULL でなければ描画する if (bmpInfoHeader != NULL) { SetDIBitsToDevice( hdc , 0 , 0 , bmpInfoHeader->biWidth , bmpInfoHeader->biHeight , 0 , 0 , 0 , bmpInfoHeader->biHeight , bmpInfoHeader + 1 , (BITMAPINFO *)bmpInfoHeader , DIB_RGB_COLORS ); } wsprintf(text , TEXT("フレーム=%d") , scrollInfo.nPos); TextOut(hdc , 0 , 0 , text , lstrlen(text)); EndPaint(hWnd , &ps); return 0;
表示するフレームの選択は、ウィンドウ下部の水平スクロールバーによって行われています。AVIファイルを開くと、最初にInitAVIResource()
関数を呼び出し、関数が成功すればストリームからフレームの長さを取得して、これをスクロールバーの最大値に設定することで、スクロールバーをフレーム選択に利用しています。
スクロールバーを操作するとWM_HSCROLL
メッセージが発行されるので、このタイミングで移動されたスクロールバーの位置を取得します。スクロールバーの位置はAVIストリームのフレームに関連付けているため、スクロールバーの位置に対応したフレームを解凍してbmpInfoHeader
変数を更新します。
以下は、メニューで[AVIを開く]と[ビットマップで保存]を選択したときの処理と、水平スクロールバーの処理です。
case WM_COMMAND: switch (LOWORD(wParam)) { case IDM_FILE_OPEN: //AVIを開く if (GetOpenFileName(&openFileName)) { RemoveAVIResource(); if (!InitAVIResource(openFileName.lpstrFile)) { //失敗した break; } //スクロール情報を初期化 scrollInfo.fMask = SIF_RANGE | SIF_POS; scrollInfo.nPos = 0; scrollInfo.nMin = 0; scrollInfo.nMax = AVIStreamLength(aviStream) - 1; //スクロールバーを初期位置へ移動 SetScrollInfo(hWnd, SB_HORZ, &scrollInfo, TRUE); //ウィンドウを再描画 InvalidateRect(hWnd, NULL, TRUE); } break; case IDM_FILE_SAVE: //ビットマップで保存 //カレントフレームが NULL ならば何もしない if (bmpInfoHeader == NULL) break; if (GetSaveFileName(&saveFileName)) { HANDLE handle; BITMAPFILEHEADER bitmapFileHeader = { 0x4D42, 0, 0, 0, 0}; DWORD dwResult; //ビットマップファイルヘッダーを初期化 bitmapFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); bitmapFileHeader.bfSize = bitmapFileHeader.bfOffBits + bmpInfoHeader->biSizeImage; //ディスクファイルに書き込み処理 handle = CreateFile( saveFileName.lpstrFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); WriteFile(handle, &bitmapFileHeader, sizeof(BITMAPFILEHEADER), &dwResult, NULL); WriteFile(handle, bmpInfoHeader, sizeof(BITMAPINFOHEADER), &dwResult, NULL); WriteFile(handle, bmpInfoHeader + 1, bmpInfoHeader->biSizeImage, &dwResult, NULL); CloseHandle(handle); } break; } case WM_HSCROLL: //スクロールバーからのメッセージ if (LOWORD(wParam) == SB_THUMBTRACK) { //スクロールバーが移動した DWORD dwCount = HIWORD(wParam); bmpInfoHeader = (BITMAPINFOHEADER *)AVIStreamGetFrame(aviFrame , dwCount); //スクロールバーを設定する scrollInfo.fMask = SIF_POS; scrollInfo.nPos = dwCount; SetScrollInfo(hWnd , SB_HORZ , &scrollInfo , TRUE); //フレームを描画するためにウィンドウを再描画 InvalidateRect(hWnd, NULL, FALSE); } break;
AVIファイルを選択すると、最初に既存のAVIリソースを解放するためにRemoveAVIResource()
を呼び出して、グローバル変数に保存されている有効なハンドルをすべて解放します。次にInitAVIResource()
関数を呼び出して、ユーザーが選択したAVIファイルを開き、必要な初期化処理を行います。そして、スクロールバーを0の位置に初期化してウィンドウを再描画しています。
ビットマップファイルを保存する場合、まずbmpInfoHeader
が有効かどうかを調べ、カレントフレームがNULL
の状態であれば無効と判断して処理を中断させます。カレントフレームが選択されていればユーザーに保存するファイル名を選択してもらい、通常のファイル入出力APIを使ってDIBをビットマップファイルに保存しています。
水平スクロールバーの処理では、スクロールバーからのメッセージがドラッグしたことを表すSB_THUMBTRACK
の場合に、スクロールバーの位置情報を取得して、位置に対応するフレームをAVIStreamGetFrame()
関数から取得しています。この戻り値をグローバル変数のbmpInfoHeader
に保存してカレントフレームを上書きすることで、ウィンドウに表示されるフレームを更新することができます。
今回は複雑なコードを避けるために、スライダーの代用にウィンドウの水平スクロールバーを使っています。しかし、スクロールバーの位置情報は16ビットしかないので、65535以上のフレームを持つAVIファイルを開いた場合、65535以上のフレームを選択すると不具合が発生します。ご了承ください。
最後に
コーデックがインストールされている環境であれば、DivXなど圧縮されたAVIファイルもVideo for Windows APIを通して安全に解凍することができます。今回は既存のAVIファイルからビットマップを抽出するだけでしたが、他にも音声ストリームからAVIファイルの音楽データを解凍してWAVEファイルとして保存するなど様々な機能があります。こうした機能を追及していけば、動画編集などのツールアプリケーションを開発することができるようになるでしょう。
マルチメディア制御に更なる興味があるのであれば、マルチメディアファイル入出力API(MMIO)やリソース交換ファイル形式 (RIFF:Resource Interchange File Format) などの世界に足を踏み入れる必要があります。他にも、MCIやオーディオミキ、MIDI、WAVEなど、マルチメディアデータを制御するAPIが多く提供されているので、動画や音声、MIDIなどの制御ソフトウェア開発に興味がある方は、ぜひMSDNなどを使ってこれらの関数を調べてみてください。
参考資料
- MSDNライブラリ 2005年4月
- 『Windows95 APIバイブル3 ODBC・マルチメディア編』 Richard J Simon・Tony Davis・John Eaton・R. Murray Goertz 著、スリーエーシステムズ 訳、江藤ソフトオフィス 監修、翔泳社、1997年5月