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を使う以外にも色々ありますが、あくまで一例として捉えていただければ結構です。