EternalWindows
MP3 / フォーマットダイアログ

指定されたファイルを圧縮または解凍して別のファイルに保存するような エンコーダ、デコーダといったアプリケーションを開発する場合は、 変換後のフォーマットをユーザーに選択させる機能を持たせると便利です。 前節のプログラムでは、変換後のフォーマットをドライバに決定してもらうためにacmFormatSuggestを呼び出していましたが、 ここで得られるフォーマットがユーザーの意思に沿わないことがあることを考慮すると、 やはりフォーマット専用のUIをユーザーに提供する必要性が出てくるでしょう。 次に示すacmFormatChosseは、フォーマット選択用のダイアログを表示し、 ユーザーが選択したフォーマットを構造体経由で返します。

MMRESULT acmFormatChoose(
  LPACMFORMATCHOOSE pfmtc  
);

pfmtcは、ACMFORMATCHOOSE構造体のアドレスを指定します。 この関数は他のACM関数と異なり、成功時に0ではなくMMSYSERR_NOERRORを返すとリファレンスには記述されていますが、 MMSYSERR_NOERRORは0として定義されているため、他のACM関数と同じように扱って問題はありません。 acmFormatChooseで列挙されるフォーマットは、優先順位の高いドライバが基になります。

次に、acmFormatChosseで表示されるダイアログを示します。

形式という名前のコンボボックスでフォーマットタグを選択し、 属性という名前のコンボボックスでサンプリングレートなどのフォーマットを選択します。 レジストリでl3codecp.acmを設定している場合、 またはLame MP3のようなMP3ドライバをインストールしている場合は、 形式という名前のコンボボックスにMPEG Layer-3が表示されます。

次に、acmFormatChosseを呼び出す例を示します。

BOOL ChooseWaveFormat(LPWAVEFORMATEX lpwf)
{
	ACMFORMATCHOOSE formatChoose;

	ZeroMemory(&formatChoose, sizeof(ACMFORMATCHOOSE));
	formatChoose.cbStruct = sizeof(ACMFORMATCHOOSE);
	formatChoose.pwfx     = lpwf;
	formatChoose.cbwfx    = sizeof(WAVEFORMATEX);

	return acmFormatChoose(&formatChoose) == MMSYSERR_NOERROR ? TRUE : FALSE;
}

この自作関数は、lpwfにユーザーが選択したフォーマットを受け取るものとします。 pwfxは、ダイアログで選択したフォーマットを受け取るWAVEFORMATEX構造体の アドレスを指定することになるため、ここではそのままlpwfを指定するように指定してます。 cbwfxは、pwfxのサイズです。 cbStructは、ACMFORMATCHOOSE構造体のサイズを代入します。 acmFormatChosseを呼び出すにあたって最低限初期化しなければならないのは、 以上の3つのメンバです。

変換先フォーマットを取得したら、次はそのフォーマットに変換可能であるかを調べてみましょう。 これは、acmStreamOpenが成功するかどうかで判断することもできますが、 acmFormatSuggestの方がドライバのテストという面において適切だといえます。

MMRESULT acmFormatSuggest(
  HACMDRIVER had,          
  LPWAVEFORMATEX pwfxSrc,  
  LPWAVEFORMATEX pwfxDst,  
  DWORD cbwfxDst,          
  DWORD fdwSuggest         
);

acmFormatSuggestには、前節で述べた最適なフォーマットを推測するということ以外に、 必要な変換先フォーマットのメンバを事前に初期化することによって、 変化の結果自体を推測させることが可能となっています。 この場合、fdwSuggestにpwfxDstのメンバを有効にするように指定し、 関数の戻り値が0であるかを調べることになります。

mmr = acmFormatSuggest(NULL, (LPWAVEFORMATEX)&mf, &wf, sizeof(WAVEFORMATEX), ACM_FORMATSUGGESTF_WFORMATTAG |
	ACM_FORMATSUGGESTF_NCHANNELS | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | ACM_FORMATSUGGESTF_WBITSPERSAMPLE);
if (mmr != 0) {
	MessageBox(NULL, TEXT("変換に失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

第5引数に指定している定数は、acmFormatSuggestに指定できる全ての定数です。 これらに相当するメンバはフォーマットの構成要素ですから、 第3引数のwfのメンバであるwFormatTag、nChannels、nSamplesPerSec、wBitsPerSampleは 適切に初期化しておかなければなりません。 nBlockAlignやnAvgBytesPerSecは関数成功時に初期化されることになっていますが、 先に示したacmFormatChosseは、これらのメンバも適切に初期化しています。

今回のプログラムは、RIFF/WAVE形式のMP3ファイルからWAVEファイルを作成すると共に、 そのWAVEフォーマットを決定するためのダイアログをacmFormatChosseで表示します。

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

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

BOOL ChooseWaveFormat(LPWAVEFORMATEX lpwf);
BOOL DecodeToWave(LPWAVEFORMATEX lpwfSrc, LPBYTE lpSrcData, DWORD dwSrcSize, LPWAVEFORMATEX lpwfDest, LPBYTE *lplpDestData, LPDWORD lpdwDestSize);
BOOL ReadMP3File(LPTSTR lpszFileName, LPMPEGLAYER3WAVEFORMAT lpmf, LPBYTE *lplpData, LPDWORD lpdwSize);
void SaveWave(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE lpData, DWORD dwSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	DWORD                dwMP3Size;
	DWORD                dwWaveSize;
	LPBYTE               lpMP3Data;
	LPBYTE               lpWaveData;
	MMRESULT             mmr;
	WAVEFORMATEX         wf;
	MPEGLAYER3WAVEFORMAT mf;

	if (!ReadMP3File(TEXT("sample.wav"), &mf, &lpMP3Data, &dwMP3Size))
		return 0;
	
	if (!ChooseWaveFormat(&wf)) {
		MessageBox(NULL, TEXT("フォーマットが選択されませんでした。"), TEXT("OK"), MB_OK);
		HeapFree(GetProcessHeap(), 0, lpMP3Data);
		return 0;
	}

	mmr = acmFormatSuggest(NULL, (LPWAVEFORMATEX)&mf, &wf, sizeof(WAVEFORMATEX), ACM_FORMATSUGGESTF_WFORMATTAG |
		ACM_FORMATSUGGESTF_NCHANNELS | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | ACM_FORMATSUGGESTF_WBITSPERSAMPLE);
	if (mmr != 0) {
		ACMFORMATDETAILS formatDetails;

		formatDetails.cbStruct    = sizeof(ACMFORMATDETAILS);
		formatDetails.dwFormatTag = wf.wFormatTag;
		formatDetails.fdwSupport  = 0;
		formatDetails.pwfx        = &wf;
		
		acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, &formatDetails.cbwfx);

		acmFormatDetails(NULL, &formatDetails, ACM_FORMATDETAILSF_FORMAT);
		MessageBox(NULL, formatDetails.szFormat, TEXT("このフォーマットには変換できません。"), MB_ICONWARNING);
		
		HeapFree(GetProcessHeap(), 0, lpMP3Data);
		return 0;
	}

	if (!DecodeToWave((LPWAVEFORMATEX)&mf, lpMP3Data, dwMP3Size, &wf, &lpWaveData, &dwWaveSize)) {
		HeapFree(GetProcessHeap(), 0, lpMP3Data);
		return 0;
	}
	
	SaveWave(TEXT("decode.wav"), &wf, lpWaveData, dwWaveSize);
	
	HeapFree(GetProcessHeap(), 0, lpMP3Data);
	HeapFree(GetProcessHeap(), 0, lpWaveData);

	MessageBox(NULL, TEXT("変換を終了しました。"), TEXT("OK"), MB_OK);
	
	return 0;
}

BOOL ChooseWaveFormat(LPWAVEFORMATEX lpwf)
{
	WAVEFORMATEX    wfEnum;
	ACMFORMATCHOOSE formatChoose;
	
	wfEnum.wFormatTag = WAVE_FORMAT_PCM;

	ZeroMemory(&formatChoose, sizeof(ACMFORMATCHOOSE));
	formatChoose.cbStruct = sizeof(ACMFORMATCHOOSE);
	formatChoose.pwfx     = lpwf;
	formatChoose.cbwfx    = sizeof(WAVEFORMATEX);
	formatChoose.pszTitle = TEXT("変換後のWAVEフォーマットを選択");
	formatChoose.fdwEnum  = ACM_FORMATENUMF_WFORMATTAG;
	formatChoose.pwfxEnum = &wfEnum;
	
	return acmFormatChoose(&formatChoose) == MMSYSERR_NOERROR ? TRUE : FALSE;
}

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;
}

BOOL ReadMP3File(LPTSTR lpszFileName, LPMPEGLAYER3WAVEFORMAT lpmf, LPBYTE *lplpData, LPDWORD lpdwSize)
{
	HMMIO    hmmio;
	MMRESULT mmr;
	MMCKINFO mmckRiff;
	MMCKINFO mmckFmt;
	MMCKINFO mmckData;
	
	hmmio = mmioOpen(lpszFileName, NULL, MMIO_READ);
	if (hmmio == NULL) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return FALSE;
	}
	
	mmckRiff.fccType = mmioStringToFOURCC(TEXT("WAVE"), 0);
	mmr = mmioDescend(hmmio, &mmckRiff, NULL, MMIO_FINDRIFF);
	if (mmr != MMSYSERR_NOERROR) {
		mmioClose(hmmio, 0);
		return FALSE;
	}
	
	mmckFmt.ckid = mmioStringToFOURCC(TEXT("fmt "), 0);
	mmioDescend(hmmio, &mmckFmt, &mmckRiff, MMIO_FINDCHUNK);
	mmioRead(hmmio, (HPSTR)lpmf, mmckFmt.cksize);
	mmioAscend(hmmio, &mmckFmt, 0);
	if (lpmf->wfx.wFormatTag != WAVE_FORMAT_MPEGLAYER3) {
		MessageBox(NULL, TEXT("RIFF/WAVE形式のMP3ファイルではありません。"), NULL, MB_ICONWARNING);
		mmioClose(hmmio, 0);
		return FALSE;
	}

	mmckData.ckid = mmioStringToFOURCC(TEXT("data"), 0);
	mmioDescend(hmmio, &mmckData, &mmckRiff, MMIO_FINDCHUNK);
	*lplpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, mmckData.cksize);
	mmioRead(hmmio, (HPSTR)*lplpData, mmckData.cksize);
	mmioAscend(hmmio, &mmckData, 0);

	mmioAscend(hmmio, &mmckRiff, 0);
	mmioClose(hmmio, 0);
	
	*lpdwSize = mmckData.cksize;

	return TRUE;
}

void SaveWave(LPTSTR lpszFileName, LPWAVEFORMATEX lpwf, LPBYTE lpData, DWORD dwSize)
{
	HMMIO    hmmio;
	MMCKINFO mmckRiff;
	MMCKINFO mmckFmt;
	MMCKINFO mmckData;

	hmmio = mmioOpen(lpszFileName, NULL, MMIO_CREATE | MMIO_WRITE);
	
	mmckRiff.fccType = mmioStringToFOURCC(TEXT("WAVE"), 0);
	mmioCreateChunk(hmmio, &mmckRiff, MMIO_CREATERIFF);

	mmckFmt.ckid = mmioStringToFOURCC(TEXT("fmt "), 0);
	mmioCreateChunk(hmmio, &mmckFmt, 0);
	mmioWrite(hmmio, (char *)lpwf, sizeof(WAVEFORMATEX));
	mmioAscend(hmmio, &mmckFmt, 0);

	mmckData.ckid = mmioStringToFOURCC(TEXT("data"), 0);
	mmioCreateChunk(hmmio, &mmckData, 0);
	mmioWrite(hmmio, (char *)lpData, dwSize);
	mmioAscend(hmmio, &mmckData, 0);
	
	mmioAscend(hmmio, &mmckRiff, 0);
	mmioClose(hmmio, 0);
}

DecodeToWaveでacmFormatSuggestを呼び出していないところに注目してください。 今回は、変換先フォーマットを事前にacmFormatChosseで初期化するため、 ドライバにフォーマットを決定してもらうコードは不要になるのです。 acmFormatChosseを内部で呼び出すChooseWaveFormatは、次のように呼び出されています。

if (!ChooseWaveFormat(&wf)) {
	MessageBox(NULL, TEXT("フォーマットが選択されませんでした。"), TEXT("OK"), MB_OK);
	HeapFree(GetProcessHeap(), 0, lpMP3Data);
	return 0;
}

wfは変換先フォーマットとして扱う変数であり、 acmFormatChosseで初期化されることから、事前に特別な初期化は必要ありません。 しかし、ダイアログに既定のフォーマットを設定したいような場合は、 事前にフォーマットを適切に初期化し、ACMFORMATCHOOSE構造体のfdwStyleに ACMFORMATCHOOSE_STYLEF_INITTOWFXSTRUCTを指定することになるでしょう。 次に、ChooseWaveFormatの内部を見てみます。

BOOL ChooseWaveFormat(LPWAVEFORMATEX lpwf)
{
	WAVEFORMATEX    wfEnum;
	ACMFORMATCHOOSE formatChoose;
	
	wfEnum.wFormatTag = WAVE_FORMAT_PCM;

	ZeroMemory(&formatChoose, sizeof(ACMFORMATCHOOSE));
	formatChoose.cbStruct = sizeof(ACMFORMATCHOOSE);
	formatChoose.pwfx     = lpwf;
	formatChoose.cbwfx    = sizeof(WAVEFORMATEX);
	formatChoose.pszTitle = TEXT("変換後のWAVEフォーマットを選択");
	formatChoose.fdwEnum  = ACM_FORMATENUMF_WFORMATTAG;
	formatChoose.pwfxEnum = &wfEnum;
	
	return acmFormatChoose(&formatChoose) == MMSYSERR_NOERROR ? TRUE : FALSE;
}

今回のように、MP3からWAVEへと変換内容が決まっている場合は、 選択できるフォーマットを限定させるのが効果的です。 fdwEnumは、pwfxEnumに指定したWAVEFORMATEX構造体のメンバの どのメンバを有効にするのかを表す定数を格納します。 pwfxEnumは、ユーザーが選択できるフォーマットを限定させるためための WAVEFORMATEX構造体のアドレスです。 上記の例では、fdwEnumをACM_FORMATENUMF_WFORMATTAGとすることで、 pwfxEnumのwFormatTagが有効になり、さらにそれがWAVE_FORMAT_PCMで初期化されて いることから、ユーザーが選択できるフォーマットのフォーマットタグは、 WAVE_FORMAT_PCMのみとなります。 この他にも、たとえば特定のサンプリングレートのみを選択したいような場合は、 サンプリングレート用のフラグとメンバを設定することになります。

wfEnum.wFormatTag     = WAVE_FORMAT_PCM;
wfEnum.nSamplesPerSec = 24000;

formatChoose.fdwEnum = ACM_FORMATENUMF_WFORMATTAG | ACM_FORMATENUMF_NSAMPLESPERSEC;

この例の場合、ダイアログで選択できるフォーマットは、 PCMでサンプリングレートが24KHzであるものに限られることになります。 サンプリングレートの値は、変換が成功するかが大きく分かれるところであり、 果たしてどの値が指定可能なのかどうかが気になるところですが、 これは前々節で紹介したプログラムを実行すれば直ぐに分かります。

最後にacmFormatSuggestが失敗したときの処理について見てみましょう。 今回のプログラムの場合、この関数が失敗するということは目的の変換先フォーマットに 変換できないということですから、どのようなフォーマットを選択したがために 失敗したのかをユーザーに通知する機能を持たせると便利です。

mmr = acmFormatSuggest(NULL, (LPWAVEFORMATEX)&mf, &wf, sizeof(WAVEFORMATEX), ACM_FORMATSUGGESTF_WFORMATTAG |
		ACM_FORMATSUGGESTF_NCHANNELS | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | ACM_FORMATSUGGESTF_WBITSPERSAMPLE);
if (mmr != 0) {
	ACMFORMATDETAILS formatDetails;
		
	formatDetails.cbStruct    = sizeof(ACMFORMATDETAILS);
	formatDetails.dwFormatTag = wf.wFormatTag;
	formatDetails.fdwSupport  = 0;
	formatDetails.pwfx        = &wf;

	acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT, &formatDetails.cbwfx);

	acmFormatDetails(NULL, &formatDetails, ACM_FORMATDETAILSF_FORMAT);
	MessageBox(NULL, formatDetails.szFormat, TEXT("このフォーマットには変換できません。"), MB_ICONWARNING);
	
	HeapFree(GetProcessHeap(), 0, lpMP3Data);
	return 0;
}

acmFormatDetailsにACM_FORMATDETAILSF_FORMATを指定して呼び出した場合、 pwfxのフォーマットを文字列化したものがszFormatに格納されます。 これを利用すれば、wsprintfでpwfxのメンバを参照して文字列を整形する必要はなくなります。 ちなみに、文字列化されたフォーマットやフォーマットタグは、 ACMFORMATCHOOSE構造体のszFormatメンバとszFormatTagメンバに格納されるため、 本来ならばそれらを参照するべきとといえます。

フォーマットダイアログのカスタマイズ

acmFormatChooseが表示するダイアログは、フォーマットを選択する機能として十分に 成り立っていますが、それ故にあと少しの機能拡張が欲しくなることがあります。 たとえば、このダイアログにはドライバを選択するコンボボックスがありませんから、 それを付け足せだけでも、使い勝手は更に上がるのではないでしょうか。 フックプロシージャの機能を利用すれば、このような要望を叶えることができるかもしれません。

formatChoose.fdwStyle  = ACMFORMATCHOOSE_STYLEF_ENABLEHOOK;
formatChoose.lCustData = NULL;
formatChoose.pfnHook   = acmFormatChooseHookProc;

fdwStyleにACMFORMATCHOOSE_STYLEF_ENABLEHOOKを指定した場合、 pfnHookとlCustDataが有効なメンバとなります。 フックプロシージャとは、ある関数に送られるメッセージをその関数より先に取得する関数で、 上記の場合、acmFormatChooseHookProcがそれに当たります。 lCustDataに指定したデータは、フックプロシージャのWM_INITDIALOGのlParamに渡ります。 次に、フックプロシージャの処理例を示します。

UINT CALLBACK acmFormatChooseHookProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {
	
	case MM_ACM_FORMATCHOOSE: {
		LPWAVEFORMATEX lpwf = (LPWAVEFORMATEX)lParam;

		if (wParam == FORMATCHOOSE_FORMAT_VERIFY && lpwf->nSamplesPerSec == 8000)
			return 1;
		break;
	}

	return 0;
}

フックプロシージャにはダイアログに送られる全てのメッセージが送られますから、 たとえば、WM_COMMANDを捕らえればボタンの押下を検出することができます。 また、フックプロシージャで0以外の値を返した場合は、 本来の関数にメッセージが送られなくなります。 MM_ACM_FORMATCHOOSEは、このフックプロシージャ専用のメッセージで、 ダイアログのコンボボックスに文字列が追加される前に送られることになっています。 メッセージが送られる回数は文字列の数に比例しており、 そこで0以外の値を返すことは文字列が追加されるのを防ぐことを意味しています。 wParamがFORMATCHOOSE_FORMAT_VERIFYである場合、追加されようとしている文字列は、 フォーマットを表すコンボボックス内における文字列であり、 そのフォーマットを調べることによって、選択するつもりのないフォーマットを除外できます。 上記の例では、サンプリングレートが8KHzのときには0を返さないよう設計していますから、 lParamで表されるフォーマットの文字列は、コンボボックスに含まれないことになります。



戻る