EternalWindows
AVI / オーディオストリームの再生

AVIファイルでは、映像や音声をストリームという単位で格納しています。 映像を格納するストリームはビデオストリームと呼ばれ、 それは一連のDIBで構成されています。 一方、音声はオーディオストリームに格納されており、 それはWAVEデータで構成されています。 ただし、DIBやWAVEデータは圧縮されている場合もあり、 そのような場合はDIBならVCM関数を、 WAVEデータならばACM関数を呼び出してデータを解凍する必要があります。 今回は、オーディオストリームの再生について取り上げます。

前節で説明したように、AVIファイルから何らかのデータを取得するにはVFWのAVIFile関数を呼び出すことになります。 これらの関数を利用するためには、最初にAVIFileInitを呼び出す必要があります。

STDAPI_(VOID) AVIFileInit(VOID); 

この関数に引数は存在しないため、単純に呼び出すだけで構いません。 STDAPIは、次のように定義されています。

#define STDAPI                  EXTERN_C HRESULT STDAPICALLTYPE
#define STDAPI_(type)           EXTERN_C type STDAPICALLTYPE

EXTERN_Cはextern "C"キーワードであり、HRESULTは戻り値の型です。 STDAPICALLTYPEは呼び出し規約の__stdcallを意味します。

アプリケーションは、AVIFile関数の呼び出しが不要になった時点で、AVIFileExitを呼び出すことになります。

STDAPI_(VOID) AVIFileExit(VOID); 

この関数もAVIFileInitと同じく引数が存在しないため、単純に呼び出すだけで構いません。

ストリームからデータを取得するには、まずストリームのハンドルを取得しなければなりません。 これには、AVIStreamOpenFromFileを呼び出します。

STDAPI AVIStreamOpenFromFile(
  PAVISTREAM * ppavi,  
  LPCTSTR szFile,      
  DWORD fccType,       
  LONG lParam,         
  UINT mode,           
  CLSID * pclsidHandler
);

ppaviは、ストリームのハンドルを受け取る変数のアドレスを指定します。 szFileは、AVIファイルのファイル名を指定します。 fccTypeは、オープンしたいストリームの種類を指定します。 streamtypeAUDIOがオーディオストリームであり、 streamtypeVIDEOがビデオストリームです。 lParamは、0で問題ないと思われます。 modeは、ファイルに対してのアクセスモードを指定します。 OF_READならば読み取りであり、OF_WRITEならば書き込み、 OF_CREATEならば作成となります。 pclsidHandlerは、使用するハンドラのCLSIDを指定します。 基本的には、NULLを指定してシステムに決定してもらいます。

ストリームに格納されているデータは、サンプルという単位で扱われることがあります。 たとえば、1サンプルはビデオストリームの場合であれば1つのDIBを表します。 また、オーディオストリームでサンプリングレートが22.05kHzならば、 1サンプルは22050バイトを表します。 これは、1秒間の音になります。 ストリームからデータを取得するには、AVIStreamReadを呼び出します。

STDAPI AVIStreamRead(
  PAVISTREAM pavi,
  LONG lStart,   
  LONG lSamples, 
  LPVOID lpBuffer,
  LONG cbBuffer, 
  LONG * plBytes,
  LONG * plSamples
);

paviは、ストリームのハンドルを指定します。 lStartは、読み取りたいサンプルの番号を指定します。 lSamplesは、読み取るサンプルの数を指定します。 lpBufferは、取得したサンプルを受け取るバッファを指定します。 cbBufferは、lpBufferのサイズを指定します plBytesは、lpBufferに書き込まれたサンプルのサイズを受け取る変数のアドレスを指定します。 不要な場合は、NULLを指定することができます。 plSamplesは、lpBufferに書き込まれたサンプル数を受け取る変数のアドレスを指定します。 不要な場合は、NULLを指定することができます。

ストリームからサンプルを取得するには、サンプルの番号が必要です。 たとえば、ビデオストリームの場合は1サンプル毎で取得する必要があるため、 先頭のサンプル番号を順に加算していくことになります。 先頭のサンプル番号は、AVIStreamStartで取得することができます。

STDAPI_(LONG) AVIStreamStart(
  PAVISTREAM pavi
);

paviは、ストリームのハンドルを指定します。 戻り値は、先頭のサンプル番号になります。

ストリームに含まれるサンプルの数は、予め知っておくべきといえます。 たとえば、ビデオストリームならば最後のサンプルを描画したことを特定できることになりますし、 オーディオストリームならば全てのサンプルを取得して再生できるようになります。 ストリームの長さは、AVIStreamLengthで取得することができます。

STDAPI_(LONG) AVIStreamLength(
  PAVISTREAM pavi
);

paviは、ストリームのハンドルを指定します。 戻り値は、サンプル単位のストリームの長さになります。

実際にサンプルを取得しても、それだけでサンプルの描画や再生ができるわけではありません。 たとえば、オーディオストリームの場合は、サンプルを再生するためにwaveOutOpenを呼び出すことになりますが、 この関数にはサンプルのフォーマットを指定しなければなりません。 フォーマットを取得するには、AVIStreamReadFormatを呼び出します。

STDAPI AVIStreamReadFormat(
  PAVISTREAM pavi,
  LONG lPos,      
  LPVOID lpFormat,
  LONG * lpcbFormat
);

paviは、ストリームのハンドルを指定します。 lPosは、フォーマットを取得する位置を指定します。 これは、0で問題ないと思われます。 lpFormatは、フォーマットを受け取るバッファを指定します。 lpcbFormatは、フォーマットのサイズを格納した変数のアドレスを指定します。 lpFormatにNULLを指定した場合は、必要なサイズが返ります。

不要になったストリームは、AVIStreamReleaseで開放することになります。

STDAPI_(LONG) AVIStreamRelease(
  PAVISTREAM pavi
);

開放したいストリームのハンドルを指定します。

今回のプログラムは、オーディオストリームからデータを取得し、それを再生します。

#include <windows.h>
#include <mmreg.h>
#include <msacm.h>
#include <vfw.h>

#pragma comment (lib, "winmm.lib")
#pragma comment (lib, "msacm32.lib")
#pragma comment (lib, "vfw32.lib")

BOOL GetAudioData(PAVISTREAM pavi, LPWAVEFORMATEX lpwfDest, LPBYTE *lplpDestData, LPDWORD lpdwDestSize);
BOOL DecodeToWave(LPWAVEFORMATEX lpwfSrc, LPBYTE lpSrcData, DWORD dwSrcSize, LPWAVEFORMATEX lpwfDest, LPBYTE *lplpDestData, LPDWORD lpdwDestSize);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static LPBYTE     lpWaveData = NULL;
	static HWAVEOUT   hwo = NULL;
	static WAVEHDR    wh = {0};
	static PAVISTREAM pavi = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		DWORD        dwDataSize;
		WAVEFORMATEX wf;
		
		AVIFileInit();
		
		if (AVIStreamOpenFromFile(&pavi, TEXT("sample.avi"), streamtypeAUDIO, 0, OF_READ, NULL) != 0) {
			MessageBox(NULL, TEXT("ファイルまたはオーディオストリームが存在しません。"), NULL, MB_ICONWARNING);
			return -1;
		}

		if (!GetAudioData(pavi, &wf, &lpWaveData, &dwDataSize))
			return -1;

		if (waveOutOpen(&hwo, WAVE_MAPPER, &wf, (DWORD_PTR)hwnd, 0, CALLBACK_WINDOW) != MMSYSERR_NOERROR)
			return -1;
		
		wh.lpData         = (LPSTR)lpWaveData;
		wh.dwBufferLength = dwDataSize;
		wh.dwFlags        = 0;

		waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR));
		waveOutWrite(hwo, &wh, sizeof(WAVEHDR));
		
		return 0;
	}
	
	case MM_WOM_DONE:
		waveOutWrite((HWAVEOUT)wParam, (LPWAVEHDR)lParam, sizeof(WAVEHDR));
		return 0;

	case WM_DESTROY:
		if (hwo != NULL) {
			waveOutReset(hwo);
			waveOutUnprepareHeader(hwo, &wh, sizeof(WAVEHDR));
			waveOutClose(hwo);
		}

		if (lpWaveData != NULL)
			HeapFree(GetProcessHeap(), 0, lpWaveData);
		
		if (pavi != NULL)
			AVIStreamRelease(pavi);
		
		AVIFileExit();

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

BOOL GetAudioData(PAVISTREAM pavi, LPWAVEFORMATEX lpwfDest, LPBYTE *lplpDestData, LPDWORD lpdwDestSize)
{
	LONG           lStart, lEnd;
	LONG           lSize;
	LPWAVEFORMATEX lpwfSrc;
	LPBYTE         lpSrcData;
	HRESULT        hr;
	
	AVIStreamReadFormat(pavi, 0, NULL, &lSize);
	lpwfSrc = (LPWAVEFORMATEX)HeapAlloc(GetProcessHeap(), 0, lSize);
	AVIStreamReadFormat(pavi, 0, lpwfSrc, &lSize);
	
	lStart = AVIStreamStart(pavi);
	lEnd  = lStart + AVIStreamLength(pavi);
	hr = AVIStreamRead(pavi, lStart, lEnd, NULL, 0, &lSize, NULL);
	if (hr != 0) {
		TCHAR szBuf[256];
		wsprintf(szBuf, TEXT("AVIStreamReadの呼び出しに失敗しました。サイズ %d エラー %x"), lSize, hr);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpwfSrc);
		return FALSE;
	}

	lpSrcData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, lSize);
	AVIStreamRead(pavi, lStart, lEnd, lpSrcData, lSize, NULL, NULL);

	if (lpwfSrc->wFormatTag != WAVE_FORMAT_PCM) {
		if (!DecodeToWave(lpwfSrc, lpSrcData, lSize, lpwfDest, lplpDestData, lpdwDestSize)) {
			HeapFree(GetProcessHeap(), 0, lpwfSrc);
			HeapFree(GetProcessHeap(), 0, lpSrcData);
			return FALSE;
		}
		HeapFree(GetProcessHeap(), 0, lpwfSrc);
		HeapFree(GetProcessHeap(), 0, lpSrcData);
	}
	else {
		*lplpDestData = lpSrcData;
		*lpwfDest = *lpwfSrc;
		*lpdwDestSize = lSize;
		HeapFree(GetProcessHeap(), 0, lpwfSrc);
	}
	
	return TRUE;
}

BOOL DecodeToWave(LPWAVEFORMATEX lpwfSrc, LPBYTE lpSrcData, DWORD dwSrcSize, LPWAVEFORMATEX lpwfDest, LPBYTE *lplpDestData, LPDWORD lpdwDestSize)
{
	HACMSTREAM      has;
	ACMSTREAMHEADER ash;
	LPBYTE          lpDestData;
	DWORD           dwDestSize;
	BOOL            bResult;
	
	lpwfDest->wFormatTag = WAVE_FORMAT_PCM;
	acmFormatSuggest(NULL, lpwfSrc, lpwfDest, sizeof(WAVEFORMATEX), ACM_FORMATSUGGESTF_WFORMATTAG);
	
	if (acmStreamOpen(&has, NULL, lpwfSrc, lpwfDest, NULL, 0, 0, ACM_STREAMOPENF_NONREALTIME) != 0) {
		MessageBox(NULL, TEXT("変換ストリームのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}

	acmStreamSize(has, dwSrcSize, &dwDestSize, ACM_STREAMSIZEF_SOURCE);
	lpDestData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDestSize);

	ZeroMemory(&ash, sizeof(ACMSTREAMHEADER));
	ash.cbStruct    = sizeof(ACMSTREAMHEADER);
	ash.pbSrc       = lpSrcData;
	ash.cbSrcLength = dwSrcSize;
	ash.pbDst       = lpDestData;
	ash.cbDstLength = dwDestSize;

	acmStreamPrepareHeader(has, &ash, 0);
	bResult = acmStreamConvert(has, &ash, 0) == 0;
	acmStreamUnprepareHeader(has, &ash, 0);
	
	acmStreamClose(has, 0);

	if (bResult) {
		*lplpDestData = lpDestData;
		*lpdwDestSize = ash.cbDstLengthUsed;
	}
	else {
		MessageBox(NULL, TEXT("変換に失敗しました。"), NULL, MB_ICONWARNING);
		*lplpDestData = NULL;
		*lpdwDestSize = 0;
		HeapFree(GetProcessHeap(), 0, lpDestData);
	}

	return bResult;
}

一連の初期化処理は、WM_CREATEで行われています。 まずAVIFileInitを呼び出し、 続いてAVIStreamOpenFromFileでオーディオストリームストリームのハンドルを取得します。 streamtypeAUDIOはオーディオストリームを意味し、 OF_READは読み取りオープンを意味します。 AVIにはビデオストリームだけを格納して、 オーディオストリームは格納していないというファイルもよく存在するため、 そのような場合はAVIStreamOpenFromFileに失敗することになります。 自作関数のGetAudioDataは、オーディオストリームからWAVEFORMATEX構造体とWAVEデータを取得します。 関数の内部を順に見ていきます。

AVIStreamReadFormat(pavi, 0, NULL, &lSize);
lpwfSrc = (LPWAVEFORMATEX)HeapAlloc(GetProcessHeap(), 0, lSize);
AVIStreamReadFormat(pavi, 0, lpwfSrc, &lSize);

このコードは、フォーマットを取得する部分です。 まず、AVIStreamReadFormatの第3引数にNULLを指定して必要なサイズを返すようにし、 2回目の呼び出して実際にフォーマットを取得します。 この例ではフォーマットを便宜上WAVEFORMATEXと見なしていますが、 実際にこの構造体が格納されているかどうかは分りません。 WAVEデータがPCMの場合ならば、PCMWAVEFORMATが格納され、 MP3で圧縮されている場合はMPEGLAYER3WAVEFORMATが格納されているでしょう。 データを取得するコードは、次のようになっています。

lStart = AVIStreamStart(pavi);
lEnd  = lStart + AVIStreamLength(pavi);
hr = AVIStreamRead(pavi, lStart, lEnd, NULL, 0, &lSize, NULL);
if (hr != 0) {
	TCHAR szBuf[256];
	wsprintf(szBuf, TEXT("AVIStreamReadの呼び出しに失敗しました。サイズ %d エラー %x"), lSize, hr);
	MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	HeapFree(GetProcessHeap(), 0, lpwfSrc);
	return FALSE;
}

lpSrcData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, lSize);
AVIStreamRead(pavi, lStart, lEnd, lpSrcData, lSize, NULL, NULL);

データはサンプルという単位で扱われているため、 AVIStreamStartで先頭のサンプル番号を取得します。 また、AVIStreamLengthを呼び出してサンプルの総数を取得し、 これを先頭のサンプル番号と加算することで、最後のサンプル番号を取得しています。 先頭と最後のサンプル番号があれば、AVIStreamReadで全てのサンプルを取得できるため、 WAVEデータ全体を取得できることになります。 1回目の呼び出しでは必要なサイズが分からないため、第4引数にNULLを指定し、 2回目の呼び出しでデータを取得することになります。

使用するAVIファイルによっては、AVIStreamReadが0を返さずに失敗することを確認しています。 この場合、lSizeには0が格納されてバッファを確保することができませんから、 処理を続行しないようにしています。 関数の戻り値はAVIERR_ERRORになっており、どのような原因によるものかは不明です。 オーディオデータが圧縮されていないPCMでも、このような現象は確認しています。

バッファを確保できたら、フォーマットとデータの変換を行います。

if (lpwfSrc->wFormatTag != WAVE_FORMAT_PCM) {
	if (!DecodeToWave(lpwfSrc, lpSrcData, lSize, lpwfDest, lplpDestData, lpdwDestSize)) {
		HeapFree(GetProcessHeap(), 0, lpwfSrc);
		HeapFree(GetProcessHeap(), 0, lpSrcData);
		return FALSE;
	}
	HeapFree(GetProcessHeap(), 0, lpwfSrc);
	HeapFree(GetProcessHeap(), 0, lpSrcData);
}
else {
	*lplpDestData = lpSrcData;
	*lpwfDest = *lpwfSrc;
	*lpdwDestSize = lSize;
	HeapFree(GetProcessHeap(), 0, lpwfSrc);
}

取得したフォーマットのタグがWAVE_FORMAT_PCMでない場合、 取得したWAVEデータがPCM形式でないことを意味しています。 この場合、ACMを通じてPCM形式に変換する必要があるため、 DecodeToWaveという自作関数を呼び出しています。 引数に指定している変数のSrcは変換元を表し、Destが変換先となります。 関数内部については、MP3の章で説明されています。 WAVEデータがPCM形式である場合は、単純に変換元のフォーマットとデータを 変換先に代入するだけで構いません。

WAVEフォーマットとWAVEデータを取得すれば、後はwaveOutOpenでデバイスをオープンし、 waveOutWriteでWAVEを再生できるようになります。 waveOutOpenではhwndとCALLBACK_WINDOWを指定しているため、 WindowProcにMM_WOM_DONEが送られることになります。 ここでwaveOutWriteを呼び出すことで、WAVEはループ再生されます。


戻る