FAQ6-1

Q. マルチメディアで,画像と音声の同期の取りかたは



A. 容易さ順にsndPlaySound(),MCIコマンド,waveOut系関数を使う方法がある

 マルチメディアで画像と音声の同期をとる方法は,大きくわけて三つあります.
 というか,WAVEファイル(.wavファイル)を再生する方法が三つあるので,同期をとる方法もそれぞれあります.三つともそれぞれ長所短所があり,状況によって使い分けるといいと思います.

sndPlaySound()を使う

 もっとも簡単なのが,sndPlaySound()を使う方法です..wavのファイル名と再生方法を指定するだけで,とりあえず音声を鳴らすことができる関数です.
 再生方法は,いろいろありますが,主にSND_SYNCとSND_ASYNCを使い分けて使用します.
 SND_SYNCは同期的に再生する指定で,WAVEファイルが再生し終えるまで,関数は制御を返しません.しかし 「同期的」だからといって,絵と音が同期できるわけではなく,プログラムの流れと同期しているだけなので,これは使えません.
 SND_ASYNCのほうは,非同期的に再生する指定です.WAVEファイルを再生し終えなくても,関数はただちに制御を戻します.
 これを一発呼んでおけば,あとでタイミングを合わせて[SetTimer()などを使って],画像を動かしてやれば,できそうな感じです.
 この方法は手軽でよいのですが,あまり正確にはできそうにありません.WAVEファイルを再生する前にファイルからロードする時間もあるでしょうし,画像が追い付かない場合にWAVEファイルの再生を遅らせるという操作もできません.
 リスト6-1は,sndPlaySound()を使ったサンプルプログラムです.GetTickCount()を使って適当にタイミングを取ってみましたが,WAVEファイルの長さもわからないので,全然ばらばらに動いてしまいます.
 32ビットアプリケーションの場合は,「同期的な再生」であっても,マルチスレッドを使えば,うまいことできるかもしれませんが,筆者はまだ試していません.

[リスト6-1] :

#define LOOPMAX	5

//sndPlaySound(高レベルオーディオ)
sample4(){
	if(1){
		//●SND_SYNCで呼び出す
		//・再生中はプログラム停止してしまう
		//・一つのWAVEファイルを最後まで再生し終えなくてはいけない
		SetWindowText( hwnd, "sndPlaySound, SND_SYNC" );
		Animate( TRUE );
		for( int i = 0; i < LOOPMAX; i ++ ) {
			sndPlaySound( "tada.wav", SND_SYNC );
			Animate();
		}
	}
	if(1){
		//●SND_ASYNCで呼び出す
		//・再生中、プログラムは継続実行される
		//・waveの長さを事前に知っておかなくてはいけない
		//・できたとしても、waveと絵の同期が完全ではない。
		SetWindowText( hwnd, "sndPlaySound, SND_ASYNC" );
		Animate( TRUE );
		for( int i = 0; i < LOOPMAX; i ++ ) {
			sndPlaySound( "tada.wav", SND_ASYNC );
			for( int j = 0; j < 4; j ++ ) {
				Animate2( TRUE );
				DWORD	dwTime = GetTickCount();
				while( dwTime + 500 > GetTickCount() ) {
					//この間に別の処理が可能
					//実際はSetTimer()を使ったほうがよい
					Animate2();
				}
				Animate();
			}
		}
	}
}

/////////////////////////////////////////
// Animate()
// 動画の1コマを再生するダミーの関数です
/////////////////////////////////////////
void Animate( BOOL fInit = FALSE  ) {
	static nAni;
	if( fInit ) nAni = 0;
	nAni++;
	HDC	hdc = GetDC( hwnd );
	char	sz[100];
	wsprintf( sz, "%dコマ目 ", nAni );
	TextOut( hdc, 0, 0, sz, lstrlen(sz) );
	ReleaseDC( hwnd, hdc );
}

/////////////////////////////////////////
// Animate2()
// WAV再生中もプログラムが動き続けていることの確認のための関数です
/////////////////////////////////////////
void Animate2( BOOL fInit = FALSE  ) {
	static nAni;
	if( fInit ) nAni = 0;
	nAni++;
	HDC	hdc = GetDC( hwnd );
	BitBlt( hdc, nAni, 32, 1, 32, NULL, 0, 0,
															DSTINVERT );
	ReleaseDC( hwnd, hdc );
	MSG	msg;
	while( PeekMessage( &msg, NULL, NULL, NULL,
														PM_REMOVE ) ) {
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}
}

MCIコマンドを使う

 2番目に簡単なのが,MCIコマンドを使う方法です.MCIコマンドとは,マルチメディアに共通して使えるインタプリタ言語のようなもので,mciSendString()を使って文字列でできたコマンドを使って操作するものです.
 例えばWAVEファイルを再生したいという場合,以下のMCIコマンドを実行するとsndPlaySound()と同じことができます.
open filename.wav type waveaudio alias MYWAVE
set MYWAVE time format samples
play MYWAVE from 1 wait
close MYWAVE
 実際は,mciSendString()を使うので,一つ一つのコマンドはmciSendString()のパラメータになります.
mciSendString( "open ・・・ alias MYWAVE", NULL, 0, hwnd );
 こういった感じで,いろいろなコマンドと指定できるパラメータがあるので,sndPlaySnd()よりはいく分複雑な操作が可能になります.MCIコマンドについての詳細はヘルプなどを見ましょう.
 MCIコマンドのうち,同期をとるためには何が利用できそうか見てみると,まずplayコマンドのパラメータにfrom,toを指定すると再生開始位置と終了位置を指定できるので,これで何回かに分けて再生して,合間に動画の処理を入れてみたらできそうな気がします.
 リスト6-2は,MCIコマンドを使ったサンプルプログラムです.statusコマンドでWAVEファイルの長さを知ることができるので,それをもとに4回に分けて再生してみました.
 実際に実行してみると,途中で音が途切れてしまいました.残念...
 playコマンドの最後のwaitは,sndPalySound()のSND_SYNC(同期)と同じ意味です.waitを付けないと,非同期になります.
 ということで,次にwaitを外して非同期にし,playコマンドの最後にnotifyを付けて実行してみました.notifyだけを付けると,再生が終わった後にウィンドウにMM_MCINOTIFYメッセージを通知してくれます.
 しかも非同期なので,再生中は他のことができてしまいます.
 実際に実行してみたのですが,最初の例と同じように音が途切れてしまいました.残念...
 まだ諦め切れず,status ・・・ positionを使うことにしました.これは,現在の再生中の位置を知らせてくれるコマンドです.
 これで常に再生位置を監視しておけば,ほぼ完全な同期ができるはずです.
 というわけで,非同期で再生し,再生後,常に監視するようにしてみたところ,ようやく成功しました.しかし遅いという難点があります.

[リスト6-2] :

HWND	hwnd;
BOOL	fNotify;

//■MCIコマンド(高レベルオーディオ)
sample5(){
	mciSend( "open c:\\windows\\tada.wav type
									 waveaudio alias MYWAVE" );
	mciSend( "set MYWAVE time format samples" );
	if(1){
		//●4回に分けて再生(waitする)
		//・再生中はプログラム停止してしまう
		//・waveと絵の同期は完全になるが、音がとぎれてしまう
		SetWindowText( hwnd, "MCI, wait" );
		Animate( TRUE );
		mciSend( "status MYWAVE length" );
		DWORD len = atoi( szMciRes ) / 4;
		for( int i = 0; i < LOOPMAX; i ++ ) {
			for( int j = 0; j < 4; j ++ ) {
				char	sz[256];
				fNotify = FALSE;
				wsprintf( sz, "play MYWAVE from %d to %d 
														wait", 1 + len * j,
														len + len * j );
				mciSend( sz );
				Animate();
			}
		}
	}
	if(1){
		//●4回に分けて再生(notifyする)
		//・再生中、プログラムは継続実行される
		//・waveと絵の同期は完全になるが、音がとぎれてしまう
		SetWindowText( hwnd, "MCI, notify" );
		Animate( TRUE );
		mciSend( "status MYWAVE length" );
		DWORD len = atoi( szMciRes ) / 4;
		char	sz[256];
		for( int i = 0; i < LOOPMAX; i ++ ) {
			for( int j = 0; j < 4; j ++ ) {
				fNotify = FALSE;
				wsprintf( sz, "play MYWAVE from %d to %d 
												notify", 1 + len * j,
															len + len * j );
				mciSend( sz );
				Animate2( TRUE );
				while( TRUE ) {
					//この間に別の処理が可能
					Animate2();
					if( fNotify ) break;
				}
				Animate();
			}
		}
	}
	if(1){
		//●再生位置を監視する(1回の再生のうち4回動画する例)
		//・再生中、プログラムは継続実行される
		//・waveと絵の同期は、ほぼ正しい
		//・遅い
		SetWindowText( hwnd, "MCI, 再生位置監視" );
		Animate( TRUE );
		mciSend( "status MYWAVE length" );
		DWORD len = atoi( szMciRes ) / 4;
		for( int i = 0; i < LOOPMAX; i ++ ) {
			mciSend( "play MYWAVE from 1" );
			for( int j = 0; j < 4; j ++ ) {
				Animate2( TRUE );
				while( TRUE ) {
					//この間に別の処理が可能
					Animate2();
					if( !mciSend( "status MYWAVE position" ) )
						break;
					DWORD pos = atoi( szMciRes );
					if( pos >= len * (j+1) ) break;
				}
				Animate();
			}
		}
	}
	mciSend( "close MYWAVE" );

}

///////////////////////////
// mciSend()
// MCIコマンドを実行する関数
///////////////////////////
char	szMciRes[1024];
BOOL mciSend( LPSTR lpcmd ) {
	DWORD	dwErr;
	if( ( dwErr = mciSendString( lpcmd, szMciRes,
						sizeof(szMciRes), hwnd ) ) != 0 ) {
		char	sz[1024];
		mciGetErrorString(dwErr, sz, sizeof(sz) );
		MessageBox( NULL, sz, "", MB_OK );
		return FALSE;
	}
	return TRUE;
}

///////////////////////////
// WndProc()
// ウィンドウプロシージャ
///////////////////////////
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
	switch (message) {
		case MM_MCINOTIFY:
			fNotify = TRUE;
			return 0;
	}
	return  DefWindowProc(hwnd ,
												message ,wParam ,lParam);
}

waveOut系関数を使う

 もっとも難しくて超面倒なのが,waveOut系の関数を使う方法です.waveOut系の関数は,MCIコマンドよりもかなり複雑な操作ができます.
 MCIコマンドで最初に行おうとした「何回かに分けて再生」,完全に同期して再生することができます.
 リスト6-3はwaveOut系関数を使った例です.waveOut系関数は,波形データをメモリ上に作っておかなければいけません.これが非常に面倒です.
 ですから,WAVEファイルを読み込む場合,WAVEファイルのフォーマットを解析して波形データをメモリにロードしなくてはいけません.
 リスト6-3の最初の部分のほとんどは,ファイルからロードする部分です.結果として,pFmtにウェーブフォーマット,pDataに波形データが入ることになります.
 実際に再生する部分ですが,waveOut系関数は音声を再生するときには,一度バッファを経由してから再生するようになっています.
 それがwaveOutPrepareHeader()とwaveOutWrite()で,前者はバッファを用意する関数で,後者はそのバッファに書き込んで再生する関数です.
 このバッファは単一ではなく,たとえ再生中であっても,どんどん積み重ねてバッファを溜めることができます.この機構により,「何回かに分けて再生」も音が途切れずに可能になるわけです.
 MCIコマンドのnotifyと同じようにメッセージ(もしくはコールバック関数)を通知できるようにもなっているので,そこで同期をとることができます.
 バッファがいくつも溜まっていても,1回のバッファの再生ごとにメッセージが通知されるので,同期の心配ありません.ちなみにリスト6-3ではコールバック関数のほうを利用しています.
 さらに,waveOutGetPosition()という関数もあるので,MCIコマンドのstatus ・・・ positionと同じように再生位置を監視することも可能です.MCIコマンドよりこちらのほうが幾分か高速です.
 ・・というわけで,すべてを見てみると,正確でなくていいものならば,簡単にsndPlaySound(,SND_ASYNC)を使用して済ませ,正確でなければならない場合は,waveOut系関数を駆使して分割して再生するのがよい,と思うのですがどうでしょう?

[リスト6-3] :

//■waveOut系(低レベルオーディオ)
//エラー処理を省いてあります
sample6(){
	HMMIO		hmmio;
	MMCKINFO	ckParent;
	MMCKINFO	ckSub;
	//WAVファイルのオープン
	hmmio = mmioOpen( "c:\\windows\\tada.wav",
												NULL, MMIO_READ );
	//"WAVE"チャンク
	ckParent.fccType
				= mmioFOURCC( 'W', 'A', 'V', 'E' );
	mmioDescend( hmmio, &ckParent, NULL,
														MMIO_FINDRIFF );
	//"fmt "チャンク
	ckSub.ckid = mmioFOURCC( 'f', 'm', 't', ' ' );
	mmioDescend( hmmio, &ckSub, &ckParent,
														MMIO_FINDCHUNK );
	DWORD	dwFmt = ckSub.cksize;
	HANDLE	hFmt = GlobalAlloc( GMEM_MOVEABLE, dwFmt );
	PCMWAVEFORMAT*	pFmt =
								(PCMWAVEFORMAT*)GlobalLock( hFmt );
	mmioRead( hmmio, (LPSTR)pFmt, dwFmt );
	mmioAscend( hmmio, &ckSub, 0 );
	//"data"チャンク
	ckSub.ckid = mmioFOURCC( 'd', 'a', 't', 'a' );
	mmioDescend( hmmio, &ckSub, &ckParent,
															MMIO_FINDCHUNK );
	DWORD	dwData = ckSub.cksize;
	HANDLE	hData 
					= GlobalAlloc( GMEM_MOVEABLE, dwData );
	LPSTR	pData = (LPSTR)GlobalLock( hData );
	mmioRead( hmmio, (LPSTR)pData, dwData );
	mmioAscend( hmmio, &ckSub, 0 );
	//WAVファイルのクローズ
	mmioClose( hmmio, 0 );
	
	//WAV出力する
	int	i, j;
	HWAVEOUT	hWave;
	if( pFmt->wf.wFormatTag != WAVE_FORMAT_PCM ||
		waveOutOpen( &hWave, WAVE_MAPPER,
								(LPWAVEFORMAT)pFmt, NULL, 0,
											WAVE_FORMAT_QUERY) ) {
		//エラー:再生できるフォーマットではない
	}
	waveOutOpen( &hWave, WAVE_MAPPER,
								(LPWAVEFORMAT)pFmt, (DWORD)WaveProc, 											0, CALLBACK_FUNCTION );
	HGLOBAL  hHdr[4];
	WAVEHDR* pHdr[4];
	for( j = 0; j < 4; j ++ ) {
		hHdr[j] = GlobalAlloc( GMEM_MOVEABLE | GMEM_SHARE,
															sizeof(WAVEHDR) );
		pHdr[j] = (WAVEHDR*)GlobalLock( hHdr[j] );
	}
	if(1){
		//●4回に分けてバッファに送る
		//・再生中、プログラムは継続実行される
		//・waveと絵の同期は完全
		//・速い
		SetWindowText( hwnd, "waveOut, 分割バッファ転送" );
		Animate( TRUE );
		for( i = 0; i < LOOPMAX; i ++ ) {
			cWaveStock = 0;
			Animate2( TRUE );
			for( j = 0; j < 4; j ++ ) {
				pHdr[j]->lpData = (BYTE*)pData + 
												( dwData / 4 ) * j;
				pHdr[j]->dwBufferLength = dwData / 4;
				pHdr[j]->dwFlags = 0;
				pHdr[j]->dwLoops = 0;
				waveOutPrepareHeader( hWave, pHdr[j],
															sizeof(WAVEHDR) );
				waveOutWrite( hWave, pHdr[j],
															sizeof(WAVEHDR) );
				cWaveStock++;
			}
			while( cWaveStock != 0 ) {
				//この間に別の処理が可能
				Animate2();
			}
			for( j = 0; j < 4; j ++ ) {
				waveOutUnprepareHeader( hWave, pHdr[j],
															sizeof(WAVEHDR) );
			}
		}
	}
	if(1){
		//●再生位置を監視する(1回の再生のうち4回動画する例)
		//・再生中、プログラムは継続実行される
		//・waveと絵の同期は、ほぼ正しい
		//・少し遅い
		SetWindowText( hwnd, "waveOut, 再生位置監視" );
		Animate( TRUE );
		for( i = 0; i < LOOPMAX; i ++ ) {
			waveOutReset( hWave );
			pHdr[0]->lpData = (BYTE*)pData;
			pHdr[0]->dwBufferLength = dwData;
			pHdr[0]->dwFlags = 0;
			pHdr[0]->dwLoops = 0;
			waveOutPrepareHeader( hWave, pHdr[0],
															sizeof(WAVEHDR) );
			waveOutWrite( hWave, pHdr[0],
															sizeof(WAVEHDR) );
			cWaveStock = 1;
			for( j = 0; j < 4; j ++ ) {
				Animate2( TRUE );
				while( TRUE ) {
					//この間に別の処理が可能
					Animate2();
					MMTIME	mmtime;
					mmtime.wType = TIME_BYTES;
					waveOutGetPosition( hWave, &mmtime,
																sizeof(mmtime) );
					if( mmtime.u.cb >= (dwData / 4) * (j+1) )
						break;
				}
				if( j < 3 ) Animate();
			}
			while( cWaveStock != 0 );
			waveOutUnprepareHeader( hWave, pHdr[0],
															sizeof(WAVEHDR) );
		}
	}
	for( j = 0; j < 4; j ++ ) {
		GlobalUnlock( hHdr[j] );
		GlobalFree( hHdr[j] );
	}
	waveOutClose( hWave );
	GlobalUnlock( hFmt );
	GlobalFree( hFmt );
	GlobalUnlock( hData );
	GlobalFree( hData );
}

///////////////////////////////////////////
// WaveProc()
// WAVEの再生状況を受け取るコールバック関数
///////////////////////////////////////////
int	cWaveStock;
void CALLBACK WaveProc( HWAVE hWave, UINT uMsg,
													DWORD, DWORD, DWORD ) {
	if( uMsg == WOM_DONE ) {
		Animate2( TRUE );
		Animate();
		cWaveStock--;
	}
}

Back to FAQ main page