Windows ACMでPCMをリサンプリングする
WindowsアプリケーションからPCMデータをリサンプリングする方法についてのメモ書きです。
リサンプリング処理はプログラマが直に書いても良いですが、出来れば楽したいのでAPIを用いて実現できないかどうかと考えていました。
結果、ACMを用いれば実現できることが分かりました。
ACM自体はMP3データのデコードでも使用されます。
リサンプリングを行うためには、ACM側にリサンプリング前と後のWAVEFORMATEX構造体を指定する必要があります。
まず、acmFormatSuggest()関数にこれらの情報を指定します。
WAVEFORMATEX m_srcFotmat; WAVEFORMATEX m_dstFotmat;
MMRESULT mr; mr = acmFormatSuggest( NULL, &m_srcFotmat, &m_dstFotmat, sizeof(m_dstFotmat), ACM_FORMATSUGGESTF_WFORMATTAG | ACM_FORMATSUGGESTF_NCHANNELS | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | ACM_FORMATSUGGESTF_WBITSPERSAMPLE ); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("変換できないPCMフォーマットです。");
第1引数はNULLで構いません。
第2引数にリサンプリング前のWAVEFORMATEX構造体データ、第3引数にリサンプリング後のWAVEFORMATEX構造体データを指定します。
第4引数には第3引数に指定した変数のサイズを指定します。
第5引数にはリサンプリング前のWAVEFORMATEX構造体変数のどのメンバを見てリサンプリング後のWAVEFORMATEX構造体のデータを決定するかを指定します。
今回はPCMフォーマット、チャネル数、サンプリングレート、量子化ビット数すべてを対象にします。
関数の実行が成功すると、m_dstFotmatに適切なデータに書き換えられます。
ここでふと疑問に思った方もいらっしゃるかもしれません。
それは、リサンプリング前と後のWAVEFORMATEX構造体をユーザで指定するにもかかわらず、リサンプリング後のデータが書き換えられることです。
これは、WAVEFORMATEX構造体にはnAvgBytesPerSecメンバやnBlockAlignメンバの存在があるためです。
nAvgBytesPerSecの値はnSamplesPerSecとnBlockAlignとの積に等しくなければなりません。
nBlockAlignの値はnChannelsとwBitsPerSampleの積を8で割った値に等しくなければなりません。
これらの値をacmFormatSuggest()が適切に指定します。
リサンプリングできないフォーマットと判断した場合、acmFormatSuggest()は失敗します。
リサンプリング前と後のWAVEFORMATEX構造体データが定まったら、acmStreamOpen()関数でACMストリームを開きます。
HACMSTREAM m_hAcm;
// 変換ストリームを開く mr = acmStreamOpen(&m_hAcm, NULL, &m_srcFotmat, &m_dstFotmat, NULL, 0, 0, 0); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("変換ストリームを開けませんでした。");
第1引数にはACMハンドラを受け取る変数のポインタを指定します。
第2引数はNULLで構いません。
第3引数、第4引数にはそれぞれリサンプリング前と後のWAVEFORMATEX構造体変数のポインタを指定します。
第5~8引数は使用しないので、NULLや0を指定します。(上記ソースコード参照)
ACMストリームを開いたら、acmStreamPrepareHeader()関数でリサンプリング前と後のデータを受け取るバッファを指定します。
std::vector<char>& srcData; std::vector<char>& dstData;
dstData.assign(dstSize, char()); // 変換のための準備 ACMSTREAMHEADER ash = {0}; ash.cbStruct = sizeof(ACMSTREAMHEADER); ash.pbSrc = reinterpret_cast<lpbyte>(&srcData[0]); ash.cbSrcLength = srcData.size(); ash.pbDst = reinterpret_cast<lpbyte>(&dstData[0]); ash.cbDstLength = dstData.size(); mr = acmStreamPrepareHeader(m_hAcm, &ash, 0); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("PCMの変換準備に失敗しました。");
ACMSTREAMHEADER構造体のpbSrc、cbSrcLengthにはそれぞれリサンプリング前データが格納されたバッファへのポインタ、バッファのサイズを指定します。
pbDst、cbDstLengthにはそれぞれリサンプリング後データが格納されるバッファへのポインタ、バッファのサイズを指定します。
関数が成功したら、acmStreamConvert()関数でリサンプリングを実行します。
// 変換 mr = acmStreamConvert(m_hAcm, &ash, ACM_STREAMCONVERTF_BLOCKALIGN); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("PCMの変換に失敗しました。");
引数は上記コードのように指定すれば良いです。
リサンプリングが不要になったら、後始末を行ってリソースを開放します。
acmStreamUnprepareHeader(m_hAcm, &ash, 0); acmStreamClose(m_hAcm, 0);
これで、アプリケーション上でPCMデータのリサンプリングが実行できたことになります。
最後に、WAVEファイルのサンプリングレートを変換するサンプルソースを記します。
なお、PcmDataクラスはこちらのPcmData.hとPcmData.cppで定義されているものを流用しています。
PcmConverter.h
#ifndef _PCM_CONVERTER_H_INCLUDED_ #define _PCM_CONVERTER_H_INCLUDED_ #include <windows.h> #include <mmreg.h> #include <msacm.h> #include <vector> #include "PcmData.h" // PCMフォーマットを変換するクラス class PcmConverter { public: PcmConverter(); virtual ~PcmConverter(); void Init(const WAVEFORMATEX& srcFotmat, const WAVEFORMATEX& dstFotmat); void Cleanup(); void Convert( std::vector<char>& srcData, std::vector<char>& dstData ); void Convert(PcmData& src, PcmData& dst); private: WAVEFORMATEX m_srcFotmat; WAVEFORMATEX m_dstFotmat; HACMSTREAM m_hAcm; }; #endif // _PCM_CONVERTER_H_INCLUDED_
PcmConverter.cpp
#include "PcmConverter.h" #include <stdexcept> #pragma comment(lib, "winmm.lib") #pragma comment(lib, "msacm32.lib") // コンストラクタ PcmConverter::PcmConverter() : m_hAcm(NULL) { } // デストラクタ PcmConverter::~PcmConverter() { Cleanup(); } // 初期化 void PcmConverter::Init( const WAVEFORMATEX& srcFotmat, const WAVEFORMATEX& dstFotmat ) { // 既に初期化積みなら失敗 if ( m_hAcm != NULL ) throw std::runtime_error("変換ストリームは既に開かれています。"); m_srcFotmat = srcFotmat; m_dstFotmat = dstFotmat; MMRESULT mr; mr = acmFormatSuggest( NULL, &m_srcFotmat, &m_dstFotmat, sizeof(m_dstFotmat), ACM_FORMATSUGGESTF_WFORMATTAG | ACM_FORMATSUGGESTF_NCHANNELS | ACM_FORMATSUGGESTF_NSAMPLESPERSEC | ACM_FORMATSUGGESTF_WBITSPERSAMPLE ); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("変換できないPCMフォーマットです。"); // 変換ストリームを開く mr = acmStreamOpen(&m_hAcm, NULL, &m_srcFotmat, &m_dstFotmat, NULL, 0, 0, 0); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("変換ストリームを開けませんでした。"); } // 後始末 void PcmConverter::Cleanup() { if ( m_hAcm != NULL ) { acmStreamClose(m_hAcm, 0); m_hAcm = NULL; } } // PCMフォーマットを変換する void PcmConverter::Convert( std::vector<char>& srcData, std::vector<char>& dstData ) { // 変換後のサイズを取得する DWORD dstSize; MMRESULT mr = acmStreamSize(m_hAcm, srcData.size(), &dstSize, ACM_STREAMSIZEF_SOURCE); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("変換後のPCMサイズ取得に失敗しました。"); dstData.assign(dstSize, char()); // 変換のための準備 ACMSTREAMHEADER ash = {0}; ash.cbStruct = sizeof(ACMSTREAMHEADER); ash.pbSrc = reinterpret_cast<lpbyte>(&srcData[0]); ash.cbSrcLength = srcData.size(); ash.pbDst = reinterpret_cast<lpbyte>(&dstData[0]); ash.cbDstLength = dstData.size(); mr = acmStreamPrepareHeader(m_hAcm, &ash, 0); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("PCMの変換準備に失敗しました。"); // 変換 mr = acmStreamConvert(m_hAcm, &ash, ACM_STREAMCONVERTF_BLOCKALIGN); if ( mr != MMSYSERR_NOERROR ) throw std::runtime_error("PCMの変換に失敗しました。"); // 変換終了 acmStreamUnprepareHeader(m_hAcm, &ash, 0); } // PCMフォーマットを変換する void PcmConverter::Convert(PcmData& src, PcmData& dst) { Init(src.GetFormat(), dst.GetFormat()); Convert(src.GetData(), dst.GetData()); dst.GetFormat() = m_dstFotmat; }
Main.cpp
#include "PcmData.h" #include "PcmConverter.h" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) try { // WAVEファイル読み込み PcmData srcPcm; srcPcm.Load("test.wav"); // サンプリングレートを変更 PcmData dstPcm; WAVEFORMATEX dstFormat = srcPcm.GetFormat(); dstFormat.nSamplesPerSec = 96000; // 96kHzに変更 dstPcm.SetFormat(dstFormat); // PCMフォーマットを変換 PcmConverter conv; conv.Convert(srcPcm, dstPcm); // WAVEファイル作成 dstPcm.Save("out.wav"); MessageBox(NULL, TEXT("WAVEファイルの作成に成功しました。"), TEXT("OK"), MB_OK | MB_ICONINFORMATION); return 0; } catch ( std::exception& e ) { MessageBoxA(NULL, e.what(), NULL, MB_OK | MB_ICONSTOP); }
test.wavファイルを読み込み、96kHzにリサンプリングしてout.wavに保存するプログラムです。
Main.cppの次の部分を変更することで、リサンプル後のフォーマットをお好みのものに変えられます。
dstFormat.nSamplesPerSec = 96000; // 96kHzに変更
PCMデータのリサンプリング方法はACMを使う以外にも色々ありますが、あくまで一例として捉えていただければ結構です。